mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-04 00:50:18 +00:00
Compare commits
12 Commits
develop
...
fix-availa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe4ca0126b | ||
|
|
bec7b29e08 | ||
|
|
ce0cdfb492 | ||
|
|
99e2538b25 | ||
|
|
6f301b1545 | ||
|
|
e5e2176da8 | ||
|
|
4e7d5aaced | ||
|
|
e4393ff16b | ||
|
|
9068fe93cf | ||
|
|
754462801e | ||
|
|
4577930312 | ||
|
|
f084d72d84 |
32
.github/helper/install.sh
vendored
32
.github/helper/install.sh
vendored
@@ -4,36 +4,6 @@ set -e
|
||||
|
||||
cd ~ || exit
|
||||
|
||||
# Authenticate git against github.com with the job token: anonymous git-over-HTTPS from the
|
||||
# runners gets throttled to a 401, which kills whichever clone is in flight — the frappe fetch
|
||||
# below, or payments under `bench get-app`. See the PR description.
|
||||
#
|
||||
# A credential helper rather than a url.insteadOf rewrite, because `git clone` PERSISTS a
|
||||
# rewritten URL into the new repo's .git/config: an insteadOf would leave the token sitting in
|
||||
# apps/payments/.git/config on the runner. A helper is consulted only when github.com actually
|
||||
# challenges, and leaves the stored remote URL untouched. Passing it through GIT_CONFIG_* keeps
|
||||
# the token out of ~/.gitconfig too, and child processes inherit it (bench shells out to git).
|
||||
ci_github_token=${CI_GITHUB_TOKEN:-${GITHUB_TOKEN:-}}
|
||||
if [ -n "$ci_github_token" ]; then
|
||||
export CI_GITHUB_TOKEN="$ci_github_token"
|
||||
export GIT_CONFIG_COUNT=3
|
||||
# Reset first: git runs EVERY configured helper and calls `store` on them after a successful
|
||||
# auth, so a `credential.helper=store` inherited from the image's gitconfig would write the
|
||||
# token to ~/.git-credentials. An empty value clears the list before ours is added.
|
||||
export GIT_CONFIG_KEY_0="credential.helper"
|
||||
export GIT_CONFIG_VALUE_0=""
|
||||
export GIT_CONFIG_KEY_1="credential.https://github.com.username"
|
||||
export GIT_CONFIG_VALUE_1="x-access-token"
|
||||
export GIT_CONFIG_KEY_2="credential.https://github.com.helper"
|
||||
# Single-quoted: $CI_GITHUB_TOKEN is expanded by the shell git runs the helper in, so the
|
||||
# token is read from the environment at call time and never stored anywhere. Answering only
|
||||
# `get` makes the helper inert for git's `store`/`erase` calls.
|
||||
export GIT_CONFIG_VALUE_2='!f() { test "$1" = get && echo "password=$CI_GITHUB_TOKEN"; }; f'
|
||||
fi
|
||||
|
||||
# Whatever happens, never sit on a credential prompt: fail fast and legibly instead.
|
||||
export GIT_TERMINAL_PROMPT=0
|
||||
|
||||
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
|
||||
frappeuser=${FRAPPE_USER:-"frappe"}
|
||||
frappecommitish=${FRAPPE_BRANCH:-}
|
||||
@@ -218,7 +188,7 @@ restore_warm_bench() {
|
||||
# Phase 1 already fetched ~/frappe to the exact live develop SHA. Fetch that commit
|
||||
# straight from it (bench init names the remote 'upstream', not 'origin', and points
|
||||
# it at this local clone — so a plain `git fetch origin` does not work).
|
||||
git fetch --no-tags --update-shallow "$HOME/frappe" HEAD || exit 1
|
||||
git fetch --no-tags "$HOME/frappe" HEAD || exit 1
|
||||
git checkout --force FETCH_HEAD || exit 1
|
||||
); then
|
||||
echo "Fast-forward to ${frappe_sha} failed; falling back to full init"
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
name: Download translations from Crowdin
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 4 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: crowdin-download
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
download-translations:
|
||||
name: Download translations into ${{ matrix.branch }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
branch: ["develop", "version-16-hotfix"]
|
||||
|
||||
steps:
|
||||
- name: Checkout ${{ matrix.branch }}
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ matrix.branch }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download translations and open PR
|
||||
uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1
|
||||
with:
|
||||
config: crowdin.yml
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
crowdin_branch_name: "[frappe.erpnext] ${{ matrix.branch }}"
|
||||
skip_ref_checkout: true
|
||||
localization_branch_name: l10n_crowdin_${{ matrix.branch }}
|
||||
create_pull_request: true
|
||||
pull_request_base_branch_name: ${{ matrix.branch }}
|
||||
commit_message: "fix: sync translations from crowdin"
|
||||
pull_request_title: "fix: sync translations from crowdin (${{ matrix.branch }})"
|
||||
pull_request_labels: "translation, skip-release-notes"
|
||||
pull_request_reviewers: barredterra
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
@@ -1,54 +0,0 @@
|
||||
name: Upload main.pot to Crowdin
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- version-16-hotfix
|
||||
paths:
|
||||
- "erpnext/locale/main.pot"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: crowdin-upload-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
upload-sources:
|
||||
name: Upload sources from ${{ github.ref_name }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout ${{ github.ref_name }}
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Restore Crowdin cache
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: .crowdin
|
||||
key: crowdin-${{ github.ref_name }}
|
||||
restore-keys: crowdin-${{ github.ref_name }}-
|
||||
|
||||
- name: Upload main.pot to Crowdin
|
||||
uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1
|
||||
with:
|
||||
config: crowdin.yml
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
download_translations: false
|
||||
create_pull_request: false
|
||||
crowdin_branch_name: "[frappe.erpnext] ${{ github.ref_name }}"
|
||||
upload_sources_args: "--cache"
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Save Crowdin cache
|
||||
uses: actions/cache/save@v6
|
||||
if: always()
|
||||
with:
|
||||
path: .crowdin
|
||||
key: crowdin-${{ github.ref_name }}-${{ github.run_id }}
|
||||
45
.github/workflows/notify-support-on-release.yml
vendored
45
.github/workflows/notify-support-on-release.yml
vendored
@@ -1,45 +0,0 @@
|
||||
name: Notify Support on PR release
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
notify-support:
|
||||
if: >-
|
||||
github.event.issue.pull_request
|
||||
&& github.event.comment.user.id == 28699486
|
||||
&& contains(github.event.comment.body, 'This PR is included in version')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
|
||||
steps:
|
||||
- name: Notify support.frappe.io
|
||||
env:
|
||||
COMMENT_ID: ${{ github.event.comment.id }}
|
||||
PR_NUMBER: ${{ github.event.issue.number }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
SUPPORT_FRAPPE_AUTH: ${{ secrets.SUPPORT_FRAPPE_AUTH }}
|
||||
run: |
|
||||
payload="$(
|
||||
jq -n \
|
||||
--arg repository "$REPOSITORY" \
|
||||
--argjson pr_number "$PR_NUMBER" \
|
||||
--argjson comment_id "$COMMENT_ID" \
|
||||
'{
|
||||
repository: $repository,
|
||||
pr_number: $pr_number,
|
||||
comment_id: $comment_id
|
||||
}'
|
||||
)"
|
||||
|
||||
curl --fail-with-body \
|
||||
--retry 3 \
|
||||
--retry-all-errors \
|
||||
--request POST \
|
||||
--header "Authorization: $SUPPORT_FRAPPE_AUTH" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "$payload" \
|
||||
"https://support.frappe.io/api/method/notify_pr_release"
|
||||
2
.github/workflows/patch.yml
vendored
2
.github/workflows/patch.yml
vendored
@@ -121,8 +121,6 @@ jobs:
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: server
|
||||
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
|
||||
CI_GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Run Patch Tests
|
||||
run: |
|
||||
|
||||
@@ -22,6 +22,6 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: alyf-de/po-review-action@5928f84d6bc9094f9ad6e2c5780f01c0044b800e # v1.1.1
|
||||
- uses: alyf-de/po-review-action@v1.1.0
|
||||
with:
|
||||
hidden-po-files: eo.po
|
||||
|
||||
2
.github/workflows/run-individual-tests.yml
vendored
2
.github/workflows/run-individual-tests.yml
vendored
@@ -129,8 +129,6 @@ jobs:
|
||||
TYPE: server
|
||||
FRAPPE_USER: ${{ github.event.inputs.user }}
|
||||
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
|
||||
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
|
||||
CI_GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
|
||||
@@ -13,7 +13,6 @@ on:
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
- '**.po'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
3
.github/workflows/server-tests-mariadb.yml
vendored
3
.github/workflows/server-tests-mariadb.yml
vendored
@@ -13,7 +13,6 @@ on:
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
- '**.po'
|
||||
schedule:
|
||||
# Run everday at midnight UTC / 5:30 IST
|
||||
- cron: "0 0 * * *"
|
||||
@@ -102,8 +101,6 @@ jobs:
|
||||
TYPE: server
|
||||
FRAPPE_USER: ${{ github.event.inputs.user }}
|
||||
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
|
||||
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
|
||||
CI_GITHUB_TOKEN: ${{ github.token }}
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_USER_HOST: '%'
|
||||
WKHTMLTOX_DEB: /tmp/wkhtmltox.deb
|
||||
|
||||
2
.github/workflows/server-tests-postgres.yml
vendored
2
.github/workflows/server-tests-postgres.yml
vendored
@@ -103,8 +103,6 @@ jobs:
|
||||
DB: postgres
|
||||
TYPE: server
|
||||
FRAPPE_BRANCH: develop
|
||||
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
|
||||
CI_GITHUB_TOKEN: ${{ github.token }}
|
||||
BENCH_CACHE_DIR: /home/runner/bench-cache
|
||||
|
||||
- name: Warm up test data
|
||||
|
||||
@@ -55,11 +55,11 @@
|
||||
"@types/node": "^25.3.0",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"eslint": "^10.8.1",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.3",
|
||||
"globals": "^16.5.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.67.0"
|
||||
"typescript-eslint": "^8.62.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,50 +206,65 @@
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
"@eslint-community/regexpp@^4.12.2":
|
||||
"@eslint-community/regexpp@^4.12.1", "@eslint-community/regexpp@^4.12.2":
|
||||
version "4.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b"
|
||||
integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==
|
||||
|
||||
"@eslint/config-array@^0.23.5":
|
||||
version "0.23.5"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.23.5.tgz#56e86d243049195d8acc0c06a1b3dfdc3fa3de95"
|
||||
integrity sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==
|
||||
"@eslint/config-array@^0.21.2":
|
||||
version "0.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.2.tgz#f29e22057ad5316cf23836cee9a34c81fffcb7e6"
|
||||
integrity sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==
|
||||
dependencies:
|
||||
"@eslint/object-schema" "^3.0.5"
|
||||
"@eslint/object-schema" "^2.1.7"
|
||||
debug "^4.3.1"
|
||||
minimatch "^10.2.4"
|
||||
minimatch "^3.1.5"
|
||||
|
||||
"@eslint/config-helpers@^0.7.0":
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.7.0.tgz#09ee4aa07b73f059ec2d4c74bf4b2ff02b322377"
|
||||
integrity sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==
|
||||
"@eslint/config-helpers@^0.4.2":
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz#1bd006ceeb7e2e55b2b773ab318d300e1a66aeda"
|
||||
integrity sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==
|
||||
dependencies:
|
||||
"@eslint/core" "^1.2.1"
|
||||
"@eslint/core" "^0.17.0"
|
||||
|
||||
"@eslint/core@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-1.2.1.tgz#c1da7cd1b82fa8787f98b5629fb811848a1b63ce"
|
||||
integrity sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==
|
||||
"@eslint/core@^0.17.0":
|
||||
version "0.17.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.17.0.tgz#77225820413d9617509da9342190a2019e78761c"
|
||||
integrity sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.15"
|
||||
|
||||
"@eslint/js@^9.39.5":
|
||||
"@eslint/eslintrc@^3.3.6":
|
||||
version "3.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.6.tgz#d22bfd6b3a7d8e1f2c0b2f2e6de111b53ec6e13e"
|
||||
integrity sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==
|
||||
dependencies:
|
||||
ajv "^6.14.0"
|
||||
debug "^4.3.2"
|
||||
espree "^10.0.1"
|
||||
globals "^14.0.0"
|
||||
ignore "^5.2.0"
|
||||
import-fresh "^3.2.1"
|
||||
js-yaml "^4.3.0"
|
||||
minimatch "^3.1.5"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@9.39.5":
|
||||
version "9.39.5"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.5.tgz#6f2fbcff75500d229d535e0a949ae13472c84787"
|
||||
integrity sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==
|
||||
|
||||
"@eslint/object-schema@^3.0.5":
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-3.0.5.tgz#88e9bf4d11d2b19c082e78ebe7ce88724a5eb091"
|
||||
integrity sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==
|
||||
"@eslint/object-schema@^2.1.7":
|
||||
version "2.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.7.tgz#6e2126a1347e86a4dedf8706ec67ff8e107ebbad"
|
||||
integrity sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==
|
||||
|
||||
"@eslint/plugin-kit@^0.7.2":
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz#4b0962f3f2c7ce8bc98b3ecfe34525c09d2cb729"
|
||||
integrity sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==
|
||||
"@eslint/plugin-kit@^0.4.1":
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz#9779e3fd9b7ee33571a57435cf4335a1794a6cb2"
|
||||
integrity sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==
|
||||
dependencies:
|
||||
"@eslint/core" "^1.2.1"
|
||||
"@eslint/core" "^0.17.0"
|
||||
levn "^0.4.1"
|
||||
|
||||
"@floating-ui/core@^1.7.5":
|
||||
@@ -1247,11 +1262,6 @@
|
||||
dependencies:
|
||||
"@types/ms" "*"
|
||||
|
||||
"@types/esrecurse@^4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/esrecurse/-/esrecurse-4.3.1.tgz#6f636af962fbe6191b830bd676ba5986926bccec"
|
||||
integrity sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==
|
||||
|
||||
"@types/estree-jsx@^1.0.0":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18"
|
||||
@@ -1259,7 +1269,7 @@
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@^1.0.8":
|
||||
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6":
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24"
|
||||
integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==
|
||||
@@ -1317,100 +1327,100 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4"
|
||||
integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.67.0":
|
||||
version "8.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz#52f9f0e47d5a7571c4336e69bfeea581509ef2cf"
|
||||
integrity sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==
|
||||
"@typescript-eslint/eslint-plugin@8.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz#1736dcdca6cae3359d818456a47d18b674761f7f"
|
||||
integrity sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@typescript-eslint/scope-manager" "8.67.0"
|
||||
"@typescript-eslint/type-utils" "8.67.0"
|
||||
"@typescript-eslint/utils" "8.67.0"
|
||||
"@typescript-eslint/visitor-keys" "8.67.0"
|
||||
"@typescript-eslint/scope-manager" "8.62.1"
|
||||
"@typescript-eslint/type-utils" "8.62.1"
|
||||
"@typescript-eslint/utils" "8.62.1"
|
||||
"@typescript-eslint/visitor-keys" "8.62.1"
|
||||
ignore "^7.0.5"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/parser@8.67.0":
|
||||
version "8.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.67.0.tgz#0158022ec9927e0afcd58a8cc2ad57e01d892f5c"
|
||||
integrity sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==
|
||||
"@typescript-eslint/parser@8.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.62.1.tgz#d3f7ba18f1bf78bfb7256fea021d1927b48e7080"
|
||||
integrity sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.67.0"
|
||||
"@typescript-eslint/types" "8.67.0"
|
||||
"@typescript-eslint/typescript-estree" "8.67.0"
|
||||
"@typescript-eslint/visitor-keys" "8.67.0"
|
||||
"@typescript-eslint/scope-manager" "8.62.1"
|
||||
"@typescript-eslint/types" "8.62.1"
|
||||
"@typescript-eslint/typescript-estree" "8.62.1"
|
||||
"@typescript-eslint/visitor-keys" "8.62.1"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.67.0":
|
||||
version "8.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.67.0.tgz#1552db007ca9206a1c6c7acf49e210bd17a8c56f"
|
||||
integrity sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==
|
||||
"@typescript-eslint/project-service@8.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.62.1.tgz#78d880eb1cf6859b5ec263d04f95403e9f90ae47"
|
||||
integrity sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.67.0"
|
||||
"@typescript-eslint/types" "^8.67.0"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.62.1"
|
||||
"@typescript-eslint/types" "^8.62.1"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.67.0":
|
||||
version "8.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz#4d4c2da09560d10dd7d947cba2d29d14d25af16d"
|
||||
integrity sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==
|
||||
"@typescript-eslint/scope-manager@8.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz#7ee65e9a6eb3ccdc4816593a4ff38840306de88a"
|
||||
integrity sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.67.0"
|
||||
"@typescript-eslint/visitor-keys" "8.67.0"
|
||||
"@typescript-eslint/types" "8.62.1"
|
||||
"@typescript-eslint/visitor-keys" "8.62.1"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.67.0", "@typescript-eslint/tsconfig-utils@^8.67.0":
|
||||
version "8.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz#f45a3eba6b9132fb47141ec03ce2f275f1ea991d"
|
||||
integrity sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==
|
||||
"@typescript-eslint/tsconfig-utils@8.62.1", "@typescript-eslint/tsconfig-utils@^8.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz#e2b5f24fe721044189cb7e81117c96d75979d627"
|
||||
integrity sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==
|
||||
|
||||
"@typescript-eslint/type-utils@8.67.0":
|
||||
version "8.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz#96bed105275559df3bcf0449b73a6414d35c59ce"
|
||||
integrity sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==
|
||||
"@typescript-eslint/type-utils@8.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz#ebd30b13bacb13070917259a23309cf644121f9a"
|
||||
integrity sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.67.0"
|
||||
"@typescript-eslint/typescript-estree" "8.67.0"
|
||||
"@typescript-eslint/utils" "8.67.0"
|
||||
"@typescript-eslint/types" "8.62.1"
|
||||
"@typescript-eslint/typescript-estree" "8.62.1"
|
||||
"@typescript-eslint/utils" "8.62.1"
|
||||
debug "^4.4.3"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/types@8.67.0", "@typescript-eslint/types@^8.67.0":
|
||||
version "8.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.67.0.tgz#4a8d00cc1faba5c14feabc60f85b7a32652f34b6"
|
||||
integrity sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==
|
||||
"@typescript-eslint/types@8.62.1", "@typescript-eslint/types@^8.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.62.1.tgz#c58be954e483b2fc98275374d5bcb40b99842dc1"
|
||||
integrity sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.67.0":
|
||||
version "8.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz#116c3a47c06119c5a050e8851861d6497dd64bc2"
|
||||
integrity sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==
|
||||
"@typescript-eslint/typescript-estree@8.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz#98c1bb17635d5b026b24193a8d29188ac64380ff"
|
||||
integrity sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.67.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.67.0"
|
||||
"@typescript-eslint/types" "8.67.0"
|
||||
"@typescript-eslint/visitor-keys" "8.67.0"
|
||||
"@typescript-eslint/project-service" "8.62.1"
|
||||
"@typescript-eslint/tsconfig-utils" "8.62.1"
|
||||
"@typescript-eslint/types" "8.62.1"
|
||||
"@typescript-eslint/visitor-keys" "8.62.1"
|
||||
debug "^4.4.3"
|
||||
minimatch "^10.2.2"
|
||||
semver "^7.7.3"
|
||||
tinyglobby "^0.2.15"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/utils@8.67.0":
|
||||
version "8.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.67.0.tgz#3e478a3d69d330a1fc50c12746cc2ee0732ccfcd"
|
||||
integrity sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==
|
||||
"@typescript-eslint/utils@8.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.62.1.tgz#1622b75c7e6df308181dd0b44855dc4228da0457"
|
||||
integrity sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.9.1"
|
||||
"@typescript-eslint/scope-manager" "8.67.0"
|
||||
"@typescript-eslint/types" "8.67.0"
|
||||
"@typescript-eslint/typescript-estree" "8.67.0"
|
||||
"@typescript-eslint/scope-manager" "8.62.1"
|
||||
"@typescript-eslint/types" "8.62.1"
|
||||
"@typescript-eslint/typescript-estree" "8.62.1"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.67.0":
|
||||
version "8.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz#601d40af9acf82a28da2286f3edafc69bba9017f"
|
||||
integrity sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==
|
||||
"@typescript-eslint/visitor-keys@8.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz#499657d77ffafb8a99eb1d6c97847ca430234722"
|
||||
integrity sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.67.0"
|
||||
"@typescript-eslint/types" "8.62.1"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
@@ -1430,10 +1440,10 @@ acorn-jsx@^5.3.2:
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||
|
||||
acorn@^8.16.0:
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.18.0.tgz#4faf01b2d6d326bfeed97aea1f52220b5f4c1940"
|
||||
integrity sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==
|
||||
acorn@^8.15.0:
|
||||
version "8.17.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe"
|
||||
integrity sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==
|
||||
|
||||
agent-base@6:
|
||||
version "6.0.2"
|
||||
@@ -1452,6 +1462,18 @@ ajv@^6.14.0:
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ansi-styles@^4.1.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
|
||||
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
|
||||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
|
||||
argparse@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||
|
||||
aria-hidden@^1.2.4:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.6.tgz#73051c9b088114c795b1ea414e9c0fff874ffc1a"
|
||||
@@ -1484,15 +1506,28 @@ bail@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d"
|
||||
integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
balanced-match@^4.0.2:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a"
|
||||
integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==
|
||||
|
||||
baseline-browser-mapping@^2.11.12:
|
||||
version "2.11.20"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz#26078c7a4b08299656ea7ddceaebec955dc44303"
|
||||
integrity sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==
|
||||
baseline-browser-mapping@^2.10.38:
|
||||
version "2.10.40"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz#f372c8eb36ff4ad0b5e7ae467014abef124554ba"
|
||||
integrity sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.15"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.15.tgz#a6d90d54067236e5f42570a3b7378d594d9b7738"
|
||||
integrity sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
brace-expansion@^5.0.5:
|
||||
version "5.0.7"
|
||||
@@ -1501,23 +1536,16 @@ brace-expansion@^5.0.5:
|
||||
dependencies:
|
||||
balanced-match "^4.0.2"
|
||||
|
||||
brace-expansion@^5.0.8:
|
||||
version "5.0.9"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.9.tgz#7c72438809b5fa5babf54199a1f1c281a6984fcf"
|
||||
integrity sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==
|
||||
dependencies:
|
||||
balanced-match "^4.0.2"
|
||||
|
||||
browserslist@^4.24.0:
|
||||
version "4.28.8"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.8.tgz#a3c79ceb70028527e5da7dafc887f3200b5168c0"
|
||||
integrity sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==
|
||||
version "4.28.4"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.4.tgz#dd8b8167a32845ff5f8cd6ce13f5abba16cd04c9"
|
||||
integrity sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==
|
||||
dependencies:
|
||||
baseline-browser-mapping "^2.11.12"
|
||||
caniuse-lite "^1.0.30001809"
|
||||
electron-to-chromium "^1.5.402"
|
||||
node-releases "^2.0.53"
|
||||
update-browserslist-db "^1.3.0"
|
||||
baseline-browser-mapping "^2.10.38"
|
||||
caniuse-lite "^1.0.30001799"
|
||||
electron-to-chromium "^1.5.376"
|
||||
node-releases "^2.0.48"
|
||||
update-browserslist-db "^1.2.3"
|
||||
|
||||
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
|
||||
version "1.0.2"
|
||||
@@ -1527,16 +1555,29 @@ call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
|
||||
es-errors "^1.3.0"
|
||||
function-bind "^1.1.2"
|
||||
|
||||
caniuse-lite@^1.0.30001809:
|
||||
version "1.0.30001810"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz#4970b477dea3278374de9bc43aa8f5d39fc3cda2"
|
||||
integrity sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==
|
||||
callsites@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
||||
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
|
||||
|
||||
caniuse-lite@^1.0.30001799:
|
||||
version "1.0.30001800"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz#b896c773e1c39400809415162bb5320371291b36"
|
||||
integrity sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==
|
||||
|
||||
ccount@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
|
||||
integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
|
||||
|
||||
chalk@^4.0.0:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
dependencies:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
character-entities-html4@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b"
|
||||
@@ -1584,6 +1625,18 @@ cmdk@^1.1.1:
|
||||
"@radix-ui/react-id" "^1.1.0"
|
||||
"@radix-ui/react-primitive" "^2.0.2"
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
|
||||
dependencies:
|
||||
color-name "~1.1.4"
|
||||
|
||||
color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
@@ -1596,6 +1649,11 @@ comma-separated-tokens@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
|
||||
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
convert-source-map@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
|
||||
@@ -1697,10 +1755,10 @@ dunder-proto@^1.0.1:
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.2.0"
|
||||
|
||||
electron-to-chromium@^1.5.402:
|
||||
version "1.5.420"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz#fc66d26a722d6f227e2092acdf38dd55b198cb44"
|
||||
integrity sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==
|
||||
electron-to-chromium@^1.5.376:
|
||||
version "1.5.383"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.383.tgz#5bd22306497d454103b289b0fef97260c56d0855"
|
||||
integrity sha512-I2484/KkAvl8lm9VyjH2JnbOIV0d/UCqT7gbzs6l+o6Vmn9wgB66uVcKX+Vk6HrXtY6fbWTOEXuv8waDTuFNCw==
|
||||
|
||||
engine.io-client@~6.5.1:
|
||||
version "6.5.4"
|
||||
@@ -1789,13 +1847,11 @@ eslint-plugin-react-refresh@^0.5.3:
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz#0311218631193fc1ea1c37531a1e7085a813bb60"
|
||||
integrity sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==
|
||||
|
||||
eslint-scope@^9.1.2:
|
||||
version "9.1.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-9.1.2.tgz#b9de6ace2fab1cff24d2e58d85b74c8fcea39802"
|
||||
integrity sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==
|
||||
eslint-scope@^8.4.0:
|
||||
version "8.4.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82"
|
||||
integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==
|
||||
dependencies:
|
||||
"@types/esrecurse" "^4.3.1"
|
||||
"@types/estree" "^1.0.8"
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^5.2.0"
|
||||
|
||||
@@ -1804,34 +1860,42 @@ eslint-visitor-keys@^3.4.3:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
|
||||
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
|
||||
|
||||
eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^5.0.1:
|
||||
eslint-visitor-keys@^4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
|
||||
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
||||
|
||||
eslint-visitor-keys@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be"
|
||||
integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==
|
||||
|
||||
eslint@^10.8.1:
|
||||
version "10.8.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.8.1.tgz#fb37d514c19b6dd5b2d6b70169fd26fddfa97967"
|
||||
integrity sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==
|
||||
eslint@9.39.5:
|
||||
version "9.39.5"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.5.tgz#2a4e3c8b0f753196efae943c8ffaa8730fc6a3fa"
|
||||
integrity sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.8.0"
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@eslint/config-array" "^0.23.5"
|
||||
"@eslint/config-helpers" "^0.7.0"
|
||||
"@eslint/core" "^1.2.1"
|
||||
"@eslint/plugin-kit" "^0.7.2"
|
||||
"@eslint-community/regexpp" "^4.12.1"
|
||||
"@eslint/config-array" "^0.21.2"
|
||||
"@eslint/config-helpers" "^0.4.2"
|
||||
"@eslint/core" "^0.17.0"
|
||||
"@eslint/eslintrc" "^3.3.6"
|
||||
"@eslint/js" "9.39.5"
|
||||
"@eslint/plugin-kit" "^0.4.1"
|
||||
"@humanfs/node" "^0.16.6"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
"@humanwhocodes/retry" "^0.4.2"
|
||||
"@types/estree" "^1.0.6"
|
||||
ajv "^6.14.0"
|
||||
chalk "^4.0.0"
|
||||
cross-spawn "^7.0.6"
|
||||
debug "^4.3.2"
|
||||
escape-string-regexp "^4.0.0"
|
||||
eslint-scope "^9.1.2"
|
||||
eslint-visitor-keys "^5.0.1"
|
||||
espree "^11.2.0"
|
||||
esquery "^1.7.0"
|
||||
eslint-scope "^8.4.0"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
espree "^10.4.0"
|
||||
esquery "^1.5.0"
|
||||
esutils "^2.0.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
file-entry-cache "^8.0.0"
|
||||
@@ -1841,20 +1905,21 @@ eslint@^10.8.1:
|
||||
imurmurhash "^0.1.4"
|
||||
is-glob "^4.0.0"
|
||||
json-stable-stringify-without-jsonify "^1.0.1"
|
||||
minimatch "^10.2.5"
|
||||
lodash.merge "^4.6.2"
|
||||
minimatch "^3.1.5"
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.9.3"
|
||||
|
||||
espree@^11.2.0:
|
||||
version "11.2.0"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-11.2.0.tgz#01d5e47dc332aaba3059008362454a8cc34ccaa5"
|
||||
integrity sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==
|
||||
espree@^10.0.1, espree@^10.4.0:
|
||||
version "10.4.0"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837"
|
||||
integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==
|
||||
dependencies:
|
||||
acorn "^8.16.0"
|
||||
acorn "^8.15.0"
|
||||
acorn-jsx "^5.3.2"
|
||||
eslint-visitor-keys "^5.0.1"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
|
||||
esquery@^1.7.0:
|
||||
esquery@^1.5.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d"
|
||||
integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==
|
||||
@@ -2031,6 +2096,11 @@ glob-parent@^6.0.2:
|
||||
dependencies:
|
||||
is-glob "^4.0.3"
|
||||
|
||||
globals@^14.0.0:
|
||||
version "14.0.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
|
||||
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
|
||||
|
||||
globals@^16.5.0:
|
||||
version "16.5.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-16.5.0.tgz#ccf1594a437b97653b2be13ed4d8f5c9f850cac1"
|
||||
@@ -2046,6 +2116,11 @@ graceful-fs@^4.2.4:
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
|
||||
has-flag@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||
|
||||
has-symbols@^1.0.3, has-symbols@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
|
||||
@@ -2197,6 +2272,14 @@ ignore@^7.0.5:
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9"
|
||||
integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==
|
||||
|
||||
import-fresh@^3.2.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf"
|
||||
integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==
|
||||
dependencies:
|
||||
parent-module "^1.0.0"
|
||||
resolve-from "^4.0.0"
|
||||
|
||||
imurmurhash@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
||||
@@ -2272,6 +2355,13 @@ jotai@^2.20.2:
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
js-yaml@^4.3.0:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.1.tgz#01216c001d67f48e2cd560d708c7af21090a3848"
|
||||
integrity sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
jsesc@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
|
||||
@@ -2477,6 +2567,11 @@ lodash.isplainobject@^4.0.6:
|
||||
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
|
||||
integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==
|
||||
|
||||
lodash.merge@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
|
||||
longest-streak@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4"
|
||||
@@ -2990,12 +3085,12 @@ minimatch@^10.2.2:
|
||||
dependencies:
|
||||
brace-expansion "^5.0.5"
|
||||
|
||||
minimatch@^10.2.4, minimatch@^10.2.5:
|
||||
version "10.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.6.tgz#fd956bbe0b77241e9f15ac5dccb1c638060968ef"
|
||||
integrity sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==
|
||||
minimatch@^3.1.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
|
||||
integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
|
||||
dependencies:
|
||||
brace-expansion "^5.0.8"
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
ms@^2.1.3:
|
||||
version "2.1.3"
|
||||
@@ -3012,10 +3107,10 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
node-releases@^2.0.53:
|
||||
version "2.0.54"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.54.tgz#09af17d5647aa9f221ec5cf2becb95b68a981afe"
|
||||
integrity sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==
|
||||
node-releases@^2.0.48:
|
||||
version "2.0.50"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.50.tgz#597197a852071ce42fc2550e58e223242bcba969"
|
||||
integrity sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==
|
||||
|
||||
object-assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
@@ -3048,6 +3143,13 @@ p-locate@^5.0.0:
|
||||
dependencies:
|
||||
p-limit "^3.0.2"
|
||||
|
||||
parent-module@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
||||
integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
|
||||
dependencies:
|
||||
callsites "^3.0.0"
|
||||
|
||||
parse-entities@^4.0.0:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.2.tgz#61d46f5ed28e4ee62e9ddc43d6b010188443f159"
|
||||
@@ -3345,6 +3447,11 @@ remark-stringify@^11.0.0:
|
||||
mdast-util-to-markdown "^2.0.0"
|
||||
unified "^11.0.0"
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
|
||||
|
||||
rolldown@~1.2.1:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.2.3.tgz#103bdcbbd575d51265277b8b510f080827b6eb6f"
|
||||
@@ -3441,6 +3548,11 @@ stringify-entities@^4.0.0:
|
||||
character-entities-html4 "^2.0.0"
|
||||
character-entities-legacy "^3.0.0"
|
||||
|
||||
strip-json-comments@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
style-to-js@^1.0.0:
|
||||
version "1.1.21"
|
||||
resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.21.tgz#2908941187f857e79e28e9cd78008b9a0b3e0e8d"
|
||||
@@ -3455,6 +3567,13 @@ style-to-object@1.0.14:
|
||||
dependencies:
|
||||
inline-style-parser "0.2.7"
|
||||
|
||||
supports-color@^7.1.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
|
||||
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
swr@^2.4.2:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/swr/-/swr-2.5.0.tgz#186c68b08b3419a5ebba4cea53d776467c9fa2d4"
|
||||
@@ -3518,15 +3637,15 @@ type-check@^0.4.0, type-check@~0.4.0:
|
||||
dependencies:
|
||||
prelude-ls "^1.2.1"
|
||||
|
||||
typescript-eslint@^8.67.0:
|
||||
version "8.67.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.67.0.tgz#1e92de09ee0ff2d96cc0848f5e9f345ea930d963"
|
||||
integrity sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==
|
||||
typescript-eslint@^8.62.1:
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.62.1.tgz#eb93fd94d527aa04ec5b844fb0b4ada613cc7d3f"
|
||||
integrity sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.67.0"
|
||||
"@typescript-eslint/parser" "8.67.0"
|
||||
"@typescript-eslint/typescript-estree" "8.67.0"
|
||||
"@typescript-eslint/utils" "8.67.0"
|
||||
"@typescript-eslint/eslint-plugin" "8.62.1"
|
||||
"@typescript-eslint/parser" "8.62.1"
|
||||
"@typescript-eslint/typescript-estree" "8.62.1"
|
||||
"@typescript-eslint/utils" "8.62.1"
|
||||
|
||||
typescript@~5.9.3:
|
||||
version "5.9.3"
|
||||
@@ -3589,10 +3708,10 @@ unist-util-visit@^5.0.0:
|
||||
unist-util-is "^6.0.0"
|
||||
unist-util-visit-parents "^6.0.0"
|
||||
|
||||
update-browserslist-db@^1.3.0:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz#9d99fbff56c50bb11ba5fd35cece5916da595836"
|
||||
integrity sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==
|
||||
update-browserslist-db@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d"
|
||||
integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==
|
||||
dependencies:
|
||||
escalade "^3.2.0"
|
||||
picocolors "^1.1.1"
|
||||
|
||||
15
crowdin.yml
15
crowdin.yml
@@ -1,5 +1,16 @@
|
||||
preserve_hierarchy: true
|
||||
|
||||
files:
|
||||
- source: /erpnext/locale/main.pot
|
||||
translation: /erpnext/locale/%two_letters_code%.po
|
||||
pull_request_title: "fix: sync translations from crowdin"
|
||||
pull_request_labels:
|
||||
- translation
|
||||
- skip-release-notes
|
||||
pull_request_reviewers:
|
||||
- barredterra # change to your GitHub username if you copied this file
|
||||
commit_message: "fix: %language% translations"
|
||||
append_commit_message: false
|
||||
languages_mapping:
|
||||
two_letters_code:
|
||||
pt-BR: pt_BR
|
||||
zh-CN: zh
|
||||
zh-TW: zh_TW
|
||||
|
||||
@@ -122,7 +122,6 @@
|
||||
"description": "Setting Account Type helps in selecting this Account in transactions.",
|
||||
"fieldname": "account_type",
|
||||
"fieldtype": "Select",
|
||||
"in_preview": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Account Type",
|
||||
"oldfieldname": "account_type",
|
||||
@@ -204,7 +203,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-03 12:59:42.190900",
|
||||
"modified": "2026-04-14 18:14:42.202065",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
@@ -265,46 +264,6 @@
|
||||
{
|
||||
"role": "HR Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Quality Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -52,42 +52,6 @@ frappe.treeview_settings["Account"] = {
|
||||
],
|
||||
root_label: "Accounts",
|
||||
get_tree_nodes: "erpnext.accounts.utils.get_children",
|
||||
get_label: function (node) {
|
||||
// clean display name — the account number renders as a badge (see
|
||||
// onrender) instead of being glued into the name
|
||||
return frappe.utils.escape_html(node.data.account_name || node.title || node.label);
|
||||
},
|
||||
onrender: function (node) {
|
||||
if (node.is_root || !node.data) return;
|
||||
|
||||
const flags = [];
|
||||
if (node.data.account_number) {
|
||||
flags.push(frappe.ui.badge({ label: node.data.account_number }));
|
||||
}
|
||||
|
||||
const company = frappe.treeview_settings["Account"].treeview?.page?.fields_dict?.company?.get_value();
|
||||
const company_currency = company && erpnext.get_currency(company);
|
||||
if (
|
||||
node.data.account_currency &&
|
||||
company_currency &&
|
||||
node.data.account_currency !== company_currency
|
||||
) {
|
||||
flags.push(frappe.ui.badge({ label: node.data.account_currency, theme: "blue" }));
|
||||
}
|
||||
|
||||
if (node.data.freeze_account === "Yes") {
|
||||
flags.push(
|
||||
frappe.ui.badge({
|
||||
label: __("Frozen"),
|
||||
icon: "lock",
|
||||
title: __("Frozen - entries restricted"),
|
||||
theme: "orange",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
erpnext.utils.render_tree_node_flags(node, flags);
|
||||
},
|
||||
on_node_render: function (node, deep) {
|
||||
const render_balances = () => {
|
||||
for (let account of cur_tree.account_balance_data) {
|
||||
@@ -268,7 +232,7 @@ frappe.treeview_settings["Account"] = {
|
||||
frappe.treeview_settings["Account"].treeview["tree"] = treeview.tree;
|
||||
if (treeview.can_create) {
|
||||
treeview.page.set_primary_action(
|
||||
{ label: __("Add Account"), short_label: __("Add") },
|
||||
__("New"),
|
||||
function () {
|
||||
let root_company = treeview.page.fields_dict.root_company.get_value();
|
||||
if (root_company) {
|
||||
@@ -279,14 +243,13 @@ frappe.treeview_settings["Account"] = {
|
||||
treeview.new_node();
|
||||
}
|
||||
},
|
||||
"plus"
|
||||
"add"
|
||||
);
|
||||
}
|
||||
},
|
||||
toolbar: [
|
||||
{
|
||||
label: __("Add Child"),
|
||||
icon: "plus",
|
||||
condition: function (node) {
|
||||
return (
|
||||
frappe.boot.user.can_create.indexOf("Account") !== -1 &&
|
||||
@@ -309,7 +272,6 @@ frappe.treeview_settings["Account"] = {
|
||||
return !node.root && frappe.boot.user.can_read.indexOf("GL Entry") !== -1;
|
||||
},
|
||||
label: __("View Ledger"),
|
||||
icon: "book-open",
|
||||
click: function (node, btn) {
|
||||
frappe.route_options = {
|
||||
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
@@ -324,106 +286,6 @@ frappe.treeview_settings["Account"] = {
|
||||
},
|
||||
btnClass: "hidden-xs",
|
||||
},
|
||||
{
|
||||
// same label and mechanism as the Account form's Actions button:
|
||||
// NOT frappe's generic rename (Allow Rename stays off) — this is
|
||||
// ERPNext's controlled update that rebuilds the derived
|
||||
// "number - name - abbr" document name
|
||||
label: __("Update Account Name / Number"),
|
||||
icon: "text-cursor-input",
|
||||
condition: function (node) {
|
||||
return !node.is_root && frappe.model.can_write("Account");
|
||||
},
|
||||
click: function (node) {
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Update Account Number / Name"),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "account_name",
|
||||
label: __("Account Name"),
|
||||
reqd: 1,
|
||||
default: node.data.account_name,
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "account_number",
|
||||
label: __("Account Number"),
|
||||
default: node.data.account_number,
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Update"),
|
||||
primary_action(values) {
|
||||
dialog.hide();
|
||||
frappe.dom.freeze(__("Updating {0}", [node.label]));
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.account.account.update_account_number",
|
||||
args: {
|
||||
name: node.label,
|
||||
account_name: values.account_name,
|
||||
account_number: values.account_number,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.exc) return;
|
||||
const treeview = frappe.views.trees["Account"];
|
||||
node.parent_node && treeview.tree.load_children(node.parent_node);
|
||||
},
|
||||
always: function () {
|
||||
frappe.dom.unfreeze();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
dialog.show();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __("Convert to Group"),
|
||||
icon: "folder-tree",
|
||||
condition: function (node) {
|
||||
return !node.is_root && !node.expandable && frappe.model.can_write("Account");
|
||||
},
|
||||
click: function (node) {
|
||||
erpnext.accounts.convert_tree_node("Account", node, "convert_ledger_to_group");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __("Convert to Non-Group"),
|
||||
icon: "file-text",
|
||||
condition: function (node) {
|
||||
// only on groups the user has opened and found empty — a
|
||||
// group with children can't convert, so don't offer it
|
||||
return (
|
||||
!node.is_root &&
|
||||
node.expandable &&
|
||||
node.loaded &&
|
||||
!node.$ul.children().length &&
|
||||
frappe.model.can_write("Account")
|
||||
);
|
||||
},
|
||||
click: function (node) {
|
||||
erpnext.accounts.convert_tree_node("Account", node, "convert_group_to_ledger");
|
||||
},
|
||||
},
|
||||
],
|
||||
extend_toolbar: true,
|
||||
};
|
||||
|
||||
frappe.provide("erpnext.accounts");
|
||||
// shared by the Account and Cost Center tree views (defined in both files,
|
||||
// whichever loads first wins): run the doctype's whitelisted convert method,
|
||||
// then re-render the branch so the node's group/leaf state updates
|
||||
erpnext.accounts.convert_tree_node =
|
||||
erpnext.accounts.convert_tree_node ||
|
||||
function (doctype, node, method) {
|
||||
frappe.call({
|
||||
method: "run_doc_method",
|
||||
args: { dt: doctype, dn: node.label, method: method },
|
||||
callback: function (r) {
|
||||
if (r.exc) return;
|
||||
const treeview = frappe.views.trees[doctype];
|
||||
node.parent_node && treeview.tree.load_children(node.parent_node);
|
||||
frappe.show_alert({ message: __("{0} converted", [node.label]), indicator: "green" });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -16,8 +16,6 @@ frappe.ui.form.on("Accounting Dimension", {
|
||||
return {
|
||||
filters: {
|
||||
name: ["not in", invalid_doctypes],
|
||||
istable: 0,
|
||||
issingle: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -60,14 +60,6 @@ class AccountingDimension(Document):
|
||||
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
|
||||
frappe.throw(msg)
|
||||
|
||||
meta = frappe.get_meta(self.document_type)
|
||||
if meta.istable or meta.issingle:
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} cannot be used as an accounting dimension as it is not a standalone document type."
|
||||
).format(frappe.bold(self.document_type))
|
||||
)
|
||||
|
||||
exists = frappe.db.get_value("Accounting Dimension", {"document_type": self.document_type}, ["name"])
|
||||
|
||||
if exists and self.is_new():
|
||||
|
||||
@@ -51,23 +51,6 @@ class TestAccountingDimension(ERPNextTestSuite):
|
||||
self.assertEqual(gle.get("department"), "_Test Department - _TC")
|
||||
self.assertEqual(gle1.get("department"), "_Test Department - _TC")
|
||||
|
||||
def test_child_table_not_allowed_as_dimension(self):
|
||||
dimension = frappe.get_doc({"doctype": "Accounting Dimension", "document_type": "Sales Team"})
|
||||
self.assertRaises(frappe.ValidationError, dimension.insert)
|
||||
|
||||
def test_single_doctype_not_allowed_as_dimension(self):
|
||||
dimension = frappe.get_doc({"doctype": "Accounting Dimension", "document_type": "Selling Settings"})
|
||||
self.assertRaises(frappe.ValidationError, dimension.insert)
|
||||
|
||||
def test_non_scalar_dimension_value_skipped_in_gl_dict(self):
|
||||
si = create_sales_invoice(do_not_save=1)
|
||||
|
||||
si.department = "_Test Department - _TC"
|
||||
self.assertEqual(si.get_gl_dict({}).get("department"), "_Test Department - _TC")
|
||||
|
||||
si.department = ["_Test Department - _TC"]
|
||||
self.assertNotIn("department", si.get_gl_dict({}))
|
||||
|
||||
def test_mandatory(self):
|
||||
location = frappe.get_doc("Accounting Dimension", "Location")
|
||||
location.dimension_defaults[0].mandatory_for_bs = True
|
||||
|
||||
@@ -95,14 +95,13 @@
|
||||
"column_break_25",
|
||||
"reports_tab",
|
||||
"remarks_section",
|
||||
"disable_include_dimensions",
|
||||
"column_break_lvjk",
|
||||
"general_ledger_remarks_length",
|
||||
"receivable_payable_remarks_length",
|
||||
"column_break_lvjk",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"default_ageing_range",
|
||||
"column_break_ntmi",
|
||||
"receivable_payable_remarks_length",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"tab_break_dpet",
|
||||
@@ -200,12 +199,10 @@
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "System will use the latest saved Currency Exchange rate on or before the transaction date, however old it is. <br>\nUncheck to ignore rates older than Stale Days and fetch a fresh rate from the exchange rate provider instead.",
|
||||
"fieldname": "allow_stale",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Allow Stale Exchange Rates",
|
||||
"show_description_on_click": 1
|
||||
"label": "Allow Stale Exchange Rates"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
@@ -281,10 +278,10 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enabling this option prevents the creation of a new Sales Invoice when the customer has an overdue limit set and their outstanding overdue amount exceeds that limit.",
|
||||
"description": "Block a new Sales Invoice when the customer's overdue amount exceeds the Overdue Limit set on the customer.",
|
||||
"fieldname": "enable_overdue_billing_threshold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Prevent Sales Invoice when Customer is Overdue"
|
||||
"label": "Restrict Customer Over Billing"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.enable_overdue_billing_threshold",
|
||||
@@ -478,7 +475,7 @@
|
||||
{
|
||||
"fieldname": "remarks_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "General Ledger Report"
|
||||
"label": "Remarks Column Length"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -552,7 +549,7 @@
|
||||
{
|
||||
"fieldname": "accounts_receivable_payable_tuning_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounts Receivable / Payable Report"
|
||||
"label": "Accounts Receivable / Payable Tuning"
|
||||
},
|
||||
{
|
||||
"fieldname": "legacy_section",
|
||||
@@ -791,12 +788,6 @@
|
||||
"fieldname": "book_stock_expense_gl_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Book Stock Expense GL Entries"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_include_dimensions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable \"Consider Accounting Dimension\" Filter"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -805,7 +796,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-14 15:26:49.070889",
|
||||
"modified": "2026-07-15 17:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -72,7 +72,6 @@ class AccountsSettings(Document):
|
||||
default_ageing_range: DF.Data | None
|
||||
delete_linked_ledger_entries: DF.Check
|
||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||
disable_include_dimensions: DF.Check
|
||||
enable_accounting_dimensions: DF.Check
|
||||
enable_common_party_accounting: DF.Check
|
||||
enable_discounts_and_margin: DF.Check
|
||||
@@ -204,8 +203,8 @@ class AccountsSettings(Document):
|
||||
if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
|
||||
frappe.throw(
|
||||
_("You cannot enable both the settings '{0}' and '{1}'.").format(
|
||||
frappe.bold(self.meta.get_translated_label("add_taxes_from_item_tax_template")),
|
||||
frappe.bold(self.meta.get_translated_label("add_taxes_from_taxes_and_charges_template")),
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))),
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))),
|
||||
),
|
||||
title=_("Auto Tax Settings Error"),
|
||||
)
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:39.423431",
|
||||
"modified": "2024-03-27 13:06:36.896195",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank",
|
||||
@@ -118,20 +118,11 @@
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Accounts User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -269,7 +269,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2026-08-21 23:11:39.585456",
|
||||
"modified": "2026-04-11 19:46:27.609994",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
@@ -299,22 +299,6 @@
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -4,18 +4,29 @@
|
||||
import frappe
|
||||
from frappe.utils import add_months, getdate
|
||||
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
|
||||
set_default_account_for_mode_of_payment,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.tests.utils import ERPNextTestSuite, if_lending_app_installed, if_lending_app_not_installed
|
||||
|
||||
|
||||
class TestBankClearance(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
frappe.clear_cache()
|
||||
create_warehouse(
|
||||
warehouse_name="_Test Warehouse",
|
||||
properties={"parent_warehouse": "All Warehouses - _TC"},
|
||||
company="_Test Company",
|
||||
)
|
||||
create_item("_Test Item")
|
||||
create_cost_center(cost_center_name="_Test Cost Center", company="_Test Company")
|
||||
|
||||
make_bank_account()
|
||||
add_transactions()
|
||||
|
||||
@@ -128,8 +139,11 @@ def add_transactions():
|
||||
|
||||
|
||||
def make_payment_entry():
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
|
||||
supplier = create_supplier(supplier_name="_Test Supplier")
|
||||
pi = make_purchase_invoice(
|
||||
supplier="_Test Supplier",
|
||||
supplier=supplier.name,
|
||||
supplier_warehouse="_Test Warehouse - _TC",
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
uom="Nos",
|
||||
@@ -144,6 +158,10 @@ def make_payment_entry():
|
||||
|
||||
|
||||
def make_pos_sales_invoice():
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
|
||||
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
|
||||
|
||||
if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}):
|
||||
@@ -152,13 +170,13 @@ def make_pos_sales_invoice():
|
||||
)
|
||||
mode_of_payment.save()
|
||||
|
||||
customer = make_customer(customer="_Test Customer")
|
||||
|
||||
mode_of_payment = frappe.get_doc("Mode of Payment", "Wire Transfer")
|
||||
|
||||
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank Clearance - _TC")
|
||||
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer", item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1
|
||||
)
|
||||
si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1)
|
||||
si.set("payments", [])
|
||||
si.append("payments", {"mode_of_payment": "Wire Transfer", "amount": 1000})
|
||||
si.insert()
|
||||
|
||||
@@ -166,10 +166,9 @@ def get_transaction_reference(txn_data: dict) -> str:
|
||||
).strip()
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def convert_mt940_to_csv(data_import: str, mt940_file_path: str):
|
||||
doc = frappe.get_doc("Bank Statement Import", data_import)
|
||||
doc.check_permission("write")
|
||||
|
||||
_file_doc, content = get_file(mt940_file_path)
|
||||
|
||||
@@ -236,30 +235,26 @@ def convert_mt940_to_csv(data_import: str, mt940_file_path: str):
|
||||
def get_preview_from_template(
|
||||
data_import: str, import_file: str | None = None, google_sheets_url: str | None = None
|
||||
):
|
||||
bsi = frappe.get_doc("Bank Statement Import", data_import)
|
||||
bsi.check_permission()
|
||||
return bsi.get_preview_from_template(import_file, google_sheets_url)
|
||||
return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
|
||||
import_file, google_sheets_url
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def form_start_import(data_import: str):
|
||||
bsi = frappe.get_doc("Bank Statement Import", data_import)
|
||||
bsi.check_permission("write")
|
||||
return bsi.start_import()
|
||||
job_id = frappe.get_doc("Bank Statement Import", data_import).start_import()
|
||||
return job_id is not None
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_errored_template(data_import_name: str):
|
||||
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
|
||||
data_import.check_permission()
|
||||
data_import.export_errored_rows()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_import_log(data_import_name: str):
|
||||
bsi = frappe.get_doc("Bank Statement Import", data_import_name)
|
||||
bsi.check_permission()
|
||||
return bsi.download_import_log()
|
||||
return frappe.get_doc("Bank Statement Import", data_import_name).download_import_log()
|
||||
|
||||
|
||||
def is_mt940_format(content: str) -> bool:
|
||||
@@ -398,7 +393,6 @@ def get_import_status(docname: str):
|
||||
import_status = {}
|
||||
|
||||
data_import = frappe.get_doc("Bank Statement Import", docname)
|
||||
data_import.check_permission()
|
||||
import_status["status"] = data_import.status
|
||||
|
||||
logs = frappe.get_all(
|
||||
|
||||
@@ -23,6 +23,8 @@ from erpnext.tests.utils import ERPNextTestSuite, if_lending_app_installed
|
||||
|
||||
class TestBankTransaction(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
make_pos_profile()
|
||||
|
||||
# generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
|
||||
uniq_identifier = frappe.generate_hash(length=10)
|
||||
gl_account = create_gl_account("_Test Bank " + uniq_identifier)
|
||||
@@ -30,7 +32,6 @@ class TestBankTransaction(ERPNextTestSuite):
|
||||
gl_account=gl_account, bank_account_name="Checking Account " + uniq_identifier
|
||||
)
|
||||
|
||||
make_pos_profile()
|
||||
add_transactions(bank_account=bank_account)
|
||||
add_vouchers(gl_account=gl_account)
|
||||
|
||||
@@ -46,7 +47,7 @@ class TestBankTransaction(ERPNextTestSuite):
|
||||
from_date=bank_transaction.date,
|
||||
to_date=utils.today(),
|
||||
)
|
||||
self.assertIn("Conrad Electronic", [payment["party"] for payment in linked_payments])
|
||||
self.assertEqual(linked_payments[0]["party"], "Conrad Electronic")
|
||||
|
||||
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
|
||||
def test_reconcile(self):
|
||||
|
||||
@@ -729,7 +729,6 @@ def get_ordered_amount(params):
|
||||
(child.item_code == item_code)
|
||||
& (parent.docstatus == 1)
|
||||
& (child.amount > child.billed_amt)
|
||||
& (child.closed == 0)
|
||||
& (parent.status != "Closed")
|
||||
& Criterion.all(get_other_condition(params, child, parent, "Purchase Order"))
|
||||
)
|
||||
|
||||
@@ -16,8 +16,6 @@ frappe.ui.form.on("Chart of Accounts Importer", {
|
||||
() => generate_tree_preview(frm),
|
||||
() => create_import_button(frm),
|
||||
() => frm.set_df_property("chart_preview", "hidden", 0),
|
||||
// the preview is the point of this page — open it right away
|
||||
() => frm.fields_dict.chart_preview.collapse(false),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -130,6 +128,7 @@ var create_import_button = function (frm) {
|
||||
freeze_message: __("Creating Accounts..."),
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
clearInterval(frm.page["interval"]);
|
||||
frm.page.set_indicator(__("Import Successful"), "blue");
|
||||
create_reset_button(frm);
|
||||
}
|
||||
@@ -143,95 +142,42 @@ var create_reset_button = function (frm) {
|
||||
frm.page
|
||||
.set_primary_action(__("Reset"), function () {
|
||||
frm.page.clear_primary_action();
|
||||
delete frm.page["show_import_button"];
|
||||
frm.reload_doc();
|
||||
})
|
||||
.addClass("btn btn-primary");
|
||||
};
|
||||
|
||||
var generate_tree_preview = function (frm) {
|
||||
let parent = __("All Accounts");
|
||||
const wrapper = $(frm.fields_dict["chart_tree"].wrapper).empty(); // empty wrapper to load new data
|
||||
|
||||
// search + expand/collapse-all lean on frappe.ui.Tree helpers added with
|
||||
// row mode; when running against an older frappe that predates them, skip
|
||||
// this toolbar so the preview still renders (just without the extras)
|
||||
const has_row_helpers =
|
||||
typeof frappe.ui.Tree.prototype.get_expansion_state === "function" &&
|
||||
typeof frappe.ui.Tree.prototype.filter_nodes === "function";
|
||||
|
||||
let tree;
|
||||
let deep_loaded = false;
|
||||
let search_text = "";
|
||||
let update_buttons = () => {};
|
||||
|
||||
if (has_row_helpers) {
|
||||
// same toolbar anatomy as the tree view: search on the left,
|
||||
// expand/collapse-all on the right (three-state: fully collapsed ->
|
||||
// Expand All, fully expanded -> Collapse All, partially expanded -> both)
|
||||
const $toolbar = $('<div class="flex items-center gap-2 mb-2"></div>').appendTo(wrapper);
|
||||
|
||||
const search_control = frappe.ui.form.make_control({
|
||||
df: { fieldtype: "Data", fieldname: "preview_search", placeholder: __("Search") },
|
||||
parent: $toolbar,
|
||||
only_input: true,
|
||||
});
|
||||
search_control.refresh();
|
||||
$(search_control.wrapper).addClass("m-0").css("width", "220px");
|
||||
search_control.$input.addClass("input-xs");
|
||||
search_control.$input.on(
|
||||
"input",
|
||||
frappe.utils.debounce(() => {
|
||||
search_text = search_control.$input.val();
|
||||
const run = () => {
|
||||
// a newer keystroke superseded this one while the deep load ran
|
||||
if (search_text !== search_control.$input.val()) return;
|
||||
tree.filter_nodes(search_text);
|
||||
};
|
||||
if (!search_text || deep_loaded) {
|
||||
run();
|
||||
return;
|
||||
var validate_coa = function (frm) {
|
||||
if (frm.doc.import_file) {
|
||||
let parent = __("All Accounts");
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa",
|
||||
args: {
|
||||
file_name: frm.doc.import_file,
|
||||
parent: parent,
|
||||
doctype: "Chart of Accounts Importer",
|
||||
file_type: frm.doc.file_type,
|
||||
for_validate: 1,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message["show_import_button"]) {
|
||||
frm.page["show_import_button"] = Boolean(r.message["show_import_button"]);
|
||||
}
|
||||
tree.load_children(tree.root_node, true).then(() => {
|
||||
deep_loaded = true;
|
||||
run();
|
||||
});
|
||||
}, 300)
|
||||
);
|
||||
|
||||
const $actions = $('<div class="ms-auto flex items-center gap-1"></div>').appendTo($toolbar);
|
||||
update_buttons = () => {
|
||||
const state = tree.get_expansion_state();
|
||||
$expand_all.prop("disabled", !(state === "collapsed" || state === "partial"));
|
||||
$collapse_all.prop("disabled", !(state === "expanded" || state === "partial"));
|
||||
};
|
||||
// tooltip on a wrapper: a disabled es-button has pointer-events:none,
|
||||
// so hover falls through to the wrapper and the tooltip still shows
|
||||
const make_action = (icon, label, onclick) => {
|
||||
const $btn = $(
|
||||
frappe.ui.button({ icon, disabled: true, onclick, attrs: { "aria-label": label } })
|
||||
);
|
||||
const $wrapper = $('<span class="inline-flex"></span>').append($btn).appendTo($actions);
|
||||
frappe.ui.tooltip($wrapper, { text: label });
|
||||
return $btn;
|
||||
};
|
||||
var $expand_all = make_action("chevrons-up-down", __("Expand All"), () => {
|
||||
tree.load_children(tree.root_node, true).then(() => {
|
||||
deep_loaded = true;
|
||||
});
|
||||
});
|
||||
var $collapse_all = make_action("chevrons-down-up", __("Collapse All"), () => {
|
||||
tree.load_children(tree.root_node, false);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var generate_tree_preview = function (frm) {
|
||||
let parent = __("All Accounts");
|
||||
$(frm.fields_dict["chart_tree"].wrapper).empty(); // empty wrapper to load new data
|
||||
|
||||
// generate tree structure based on the csv data
|
||||
tree = new frappe.ui.Tree({
|
||||
parent: wrapper,
|
||||
return new frappe.ui.Tree({
|
||||
parent: $(frm.fields_dict["chart_tree"].wrapper),
|
||||
label: parent,
|
||||
expandable: true,
|
||||
// read-only preview: row-mode visuals without actions or hover cards
|
||||
// (ignored by an older frappe, which renders the legacy tree)
|
||||
row_style: true,
|
||||
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa",
|
||||
args: {
|
||||
file_name: frm.doc.import_file,
|
||||
@@ -239,9 +185,8 @@ var generate_tree_preview = function (frm) {
|
||||
doctype: "Chart of Accounts Importer",
|
||||
file_type: frm.doc.file_type,
|
||||
},
|
||||
on_node_render: () => update_buttons(),
|
||||
// expanded flips right after this callback — check on the next tick
|
||||
on_click: () => setTimeout(update_buttons, 0),
|
||||
onclick: function (node) {
|
||||
parent = node.value;
|
||||
},
|
||||
});
|
||||
return tree;
|
||||
};
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:40.799391",
|
||||
"modified": "2026-04-14 18:15:27.367298",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cost Center",
|
||||
@@ -181,54 +181,6 @@
|
||||
{
|
||||
"role": "HR Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Projects Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Projects User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Quality Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -12,19 +12,6 @@ frappe.treeview_settings["Cost Center"] = {
|
||||
],
|
||||
root_label: "Cost Centers",
|
||||
get_tree_nodes: "erpnext.accounts.utils.get_children",
|
||||
get_label: function (node) {
|
||||
// clean display name — the number renders as a badge (see onrender)
|
||||
return frappe.utils.escape_html(node.data.cost_center_name || node.title || node.label);
|
||||
},
|
||||
onrender: function (node) {
|
||||
if (node.is_root || !node.data) return;
|
||||
|
||||
const flags = [];
|
||||
if (node.data.cost_center_number) {
|
||||
flags.push(frappe.ui.badge({ label: node.data.cost_center_number }));
|
||||
}
|
||||
erpnext.utils.render_tree_node_flags(node, flags);
|
||||
},
|
||||
add_tree_node: "erpnext.accounts.utils.add_cc",
|
||||
menu_items: [
|
||||
{
|
||||
@@ -55,37 +42,6 @@ frappe.treeview_settings["Cost Center"] = {
|
||||
},
|
||||
],
|
||||
ignore_fields: ["parent_cost_center"],
|
||||
toolbar: [
|
||||
{
|
||||
label: __("Convert to Group"),
|
||||
icon: "folder-tree",
|
||||
condition: function (node) {
|
||||
return !node.is_root && !node.expandable && frappe.model.can_write("Cost Center");
|
||||
},
|
||||
click: function (node) {
|
||||
erpnext.accounts.convert_tree_node("Cost Center", node, "convert_ledger_to_group");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __("Convert to Non-Group"),
|
||||
icon: "file-text",
|
||||
condition: function (node) {
|
||||
// only on groups the user has opened and found empty — a
|
||||
// group with children can't convert, so don't offer it
|
||||
return (
|
||||
!node.is_root &&
|
||||
node.expandable &&
|
||||
node.loaded &&
|
||||
!node.$ul.children().length &&
|
||||
frappe.model.can_write("Cost Center")
|
||||
);
|
||||
},
|
||||
click: function (node) {
|
||||
erpnext.accounts.convert_tree_node("Cost Center", node, "convert_group_to_ledger");
|
||||
},
|
||||
},
|
||||
],
|
||||
extend_toolbar: true,
|
||||
onload: function (treeview) {
|
||||
function get_company() {
|
||||
return treeview.page.fields_dict.company.get_value();
|
||||
@@ -126,22 +82,3 @@ frappe.treeview_settings["Cost Center"] = {
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
frappe.provide("erpnext.accounts");
|
||||
// shared by the Account and Cost Center tree views (defined in both files,
|
||||
// whichever loads first wins): run the doctype's whitelisted convert method,
|
||||
// then re-render the branch so the node's group/leaf state updates
|
||||
erpnext.accounts.convert_tree_node =
|
||||
erpnext.accounts.convert_tree_node ||
|
||||
function (doctype, node, method) {
|
||||
frappe.call({
|
||||
method: "run_doc_method",
|
||||
args: { dt: doctype, dn: node.label, method: method },
|
||||
callback: function (r) {
|
||||
if (r.exc) return;
|
||||
const treeview = frappe.views.trees[doctype];
|
||||
node.parent_node && treeview.tree.load_children(node.parent_node);
|
||||
frappe.show_alert({ message: __("{0} converted", [node.label]), indicator: "green" });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:41.010871",
|
||||
"modified": "2024-11-19 16:35:11.836441",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Coupon Code",
|
||||
@@ -179,20 +179,11 @@
|
||||
"role": "Website Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "coupon_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -234,10 +234,8 @@ frappe.ui.form.on("Dunning", {
|
||||
dn: frm.doc.name,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
var doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
}
|
||||
var doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
],
|
||||
"icon": "fa fa-book",
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:42.386104",
|
||||
"modified": "2024-03-27 13:09:44.514241",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Finance Book",
|
||||
@@ -55,26 +55,13 @@
|
||||
"report": 1,
|
||||
"role": "Auditor",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"role": "HR Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Quality Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "finance_book_name",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
@@ -31,4 +31,11 @@ class TestFinanceBook(ERPNextTestSuite):
|
||||
|
||||
|
||||
def create_finance_book():
|
||||
return frappe.get_doc("Finance Book", "Test Finance Book 1")
|
||||
if not frappe.db.exists("Finance Book", "_Test Finance Book"):
|
||||
finance_book = frappe.get_doc(
|
||||
{"doctype": "Finance Book", "finance_book_name": "_Test Finance Book"}
|
||||
).insert()
|
||||
else:
|
||||
finance_book = frappe.get_doc("Finance Book", "_Test Finance Book")
|
||||
|
||||
return finance_book
|
||||
|
||||
@@ -32,7 +32,6 @@ from erpnext.accounts.doctype.financial_report_template.financial_report_validat
|
||||
AccountFilterValidator,
|
||||
CalculationFormulaValidator,
|
||||
DependencyValidator,
|
||||
get_valid_api_method,
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
get_columns,
|
||||
@@ -1183,12 +1182,10 @@ class RowProcessor:
|
||||
|
||||
def _process_api_row(self, row) -> RowData:
|
||||
api_path = row.calculation_formula
|
||||
|
||||
method = get_valid_api_method(api_path)
|
||||
# TODO
|
||||
|
||||
try:
|
||||
# nosemgrep: frappe-semgrep-rules.rules.security.frappe-codeinjection-eval
|
||||
values = frappe.call(method, filters=self.context.filters, periods=self.period_list, row=row)
|
||||
values = frappe.call(api_path, filters=self.context.filters, periods=self.period_list, row=row)
|
||||
|
||||
if row.reverse_sign:
|
||||
values = [-1 * v for v in values]
|
||||
|
||||
@@ -163,7 +163,7 @@ function show_accounts_tree(template_rows, has_selection) {
|
||||
fieldname: "company",
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
label: __("Company"),
|
||||
label: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
onchange: () => {
|
||||
@@ -176,7 +176,7 @@ function show_accounts_tree(template_rows, has_selection) {
|
||||
fieldname: "view_type",
|
||||
fieldtype: "Select",
|
||||
options: ["Missing Accounts", "Filtered Accounts"],
|
||||
label: __("View"),
|
||||
label: "View",
|
||||
default: has_selection ? "Filtered Accounts" : "Missing Accounts",
|
||||
reqd: 1,
|
||||
onchange: () => {
|
||||
@@ -192,10 +192,10 @@ function show_accounts_tree(template_rows, has_selection) {
|
||||
{
|
||||
fieldname: "tip",
|
||||
fieldtype: "HTML",
|
||||
label: __("Tip"),
|
||||
label: "Tip",
|
||||
options: `
|
||||
<div class="alert alert-success" role="alert">
|
||||
${__("Tip: Select report lines to view their accounts")}
|
||||
Tip: Select report lines to view their accounts
|
||||
</div>
|
||||
`,
|
||||
depends_on: has_selection ? "eval: false" : "eval: true",
|
||||
@@ -203,7 +203,7 @@ function show_accounts_tree(template_rows, has_selection) {
|
||||
{
|
||||
fieldname: "tree_area",
|
||||
fieldtype: "HTML",
|
||||
label: __("Chart of Accounts"),
|
||||
label: "Chart of Accounts",
|
||||
read_only: 1,
|
||||
depends_on: "eval: doc.company",
|
||||
},
|
||||
@@ -236,8 +236,6 @@ async function refresh_tree_view(dialog, account_rows) {
|
||||
parent: wrapper,
|
||||
label: company,
|
||||
root_value: company,
|
||||
// read-only preview: row-mode visuals without actions
|
||||
row_style: true,
|
||||
method: "erpnext.accounts.doctype.financial_report_template.financial_report_engine.get_children_accounts",
|
||||
args: { doctype: "Account", company: company, filtered_accounts: filtered_accounts, missed: missed },
|
||||
toolbar: [],
|
||||
@@ -290,14 +288,14 @@ function update_formula_label(frm, data_source) {
|
||||
if (!field) return;
|
||||
|
||||
const labels = {
|
||||
"Account Data": __("Account Filter"),
|
||||
"Custom API": __("API Method Path"),
|
||||
"Account Data": "Account Filter",
|
||||
"Custom API": "API Method Path",
|
||||
};
|
||||
|
||||
grid.update_docfield_property(
|
||||
"calculation_formula",
|
||||
"label",
|
||||
labels[data_source] || __("Calculation Formula")
|
||||
labels[data_source] || "Calculation Formula"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -372,7 +370,7 @@ function update_formula_description(frm, data_source) {
|
||||
description_html = `
|
||||
<div ${container_style}>
|
||||
<h5 ${title_style}>Custom API Setup</h5>
|
||||
<p ${text_style}>Path to your custom whitelisted method that returns financial data. It must permit GET requests.</p>
|
||||
<p ${text_style}>Path to your custom method that returns financial data.</p>
|
||||
|
||||
<h6 ${subtitle_style}>Format:</h6>
|
||||
<ul ${list_style}>
|
||||
@@ -382,8 +380,7 @@ function update_formula_description(frm, data_source) {
|
||||
|
||||
<h6 ${subtitle_style}>Method Signature:</h6>
|
||||
<div ${code_style}>
|
||||
<!-- is used for line breaks since frappe.render replaces newlines with spaces -->
|
||||
<pre ${pre_style} class="language-python">@frappe.whitelist(methods=["GET"]) def get_custom_data(filters, periods, row): # filters: dict — report filters (company, period, etc.) # periods: list[dict] — period definitions # row: dict — the current report row return [1000.0, 1200.0, 1150.0] # one value per period</pre>
|
||||
<pre ${pre_style}>def get_custom_data(filters, periods, row): <br> # filters: dict — report filters (company, period, etc.) <br> # periods: list[dict] — period definitions <br> # row: dict — the current report row <br><br> return [1000.0, 1200.0, 1150.0] # one value per period</pre>
|
||||
</div>
|
||||
|
||||
<h6 ${subtitle_style}>Return Format:</h6>
|
||||
|
||||
@@ -8,40 +8,17 @@ from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
from frappe import _, is_whitelisted
|
||||
from frappe import _
|
||||
from frappe.database.operator_map import OPERATOR_MAP
|
||||
|
||||
|
||||
def get_valid_api_method(api_path: str):
|
||||
"""Resolve `api_path`, ensuring it is whitelisted and permits GET (i.e. read-only)."""
|
||||
method = frappe.get_attr(api_path)
|
||||
is_whitelisted(method)
|
||||
|
||||
if "GET" not in frappe.allowed_http_methods_for_whitelisted_func.get(method, ()):
|
||||
frappe.throw(
|
||||
_("Method {0} must permit GET requests").format(frappe.bold(api_path)),
|
||||
frappe.PermissionError,
|
||||
title=_("Method Not Allowed"),
|
||||
)
|
||||
|
||||
return method
|
||||
|
||||
|
||||
def get_formula_field_label(data_source: str) -> str:
|
||||
# Must mirror the `labels` map in financial_report_template.js (update_formula_label),
|
||||
labels = {
|
||||
"Account Data": _("Account Filter"),
|
||||
"Custom API": _("API Method Path"),
|
||||
}
|
||||
return labels.get(data_source, _("Calculation Formula"))
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationIssue:
|
||||
"""Represents a single validation issue"""
|
||||
|
||||
message: str
|
||||
row_idx: int | None = None
|
||||
field: str | None = None
|
||||
details: dict[str, Any] = None
|
||||
|
||||
def __post_init__(self):
|
||||
@@ -49,9 +26,10 @@ class ValidationIssue:
|
||||
self.details = {}
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.row_idx:
|
||||
return _("Row {0}: {1}", context="Financial Report Template").format(self.row_idx, self.message)
|
||||
return self.message
|
||||
prefix = f"Row {self.row_idx}: " if self.row_idx else ""
|
||||
field_info = f"[{self.field}] " if self.field else ""
|
||||
message = f"{prefix}{field_info}{self.message}"
|
||||
return _(message)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -153,9 +131,7 @@ class TemplateStructureValidator(Validator):
|
||||
if not re.match(r"^[A-Za-z][A-Za-z0-9_-]*$", ref_code):
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_(
|
||||
"Invalid line reference format: '{0}'. Must start with letter and contain only letters, numbers, underscores, and hyphens"
|
||||
).format(ref_code),
|
||||
message=f"Invalid line reference format: '{ref_code}'. Must start with letter and contain only letters, numbers, underscores, and hyphens",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -164,7 +140,7 @@ class TemplateStructureValidator(Validator):
|
||||
if ref_code in used_codes:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Duplicate line reference: '{0}'").format(ref_code),
|
||||
message=f"Duplicate line reference: '{ref_code}'",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -180,7 +156,7 @@ class TemplateStructureValidator(Validator):
|
||||
if row.data_source == "Account Data" and not row.balance_type:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Balance Type is required for Account Data"),
|
||||
message="Balance Type is required for Account Data",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -190,9 +166,7 @@ class TemplateStructureValidator(Validator):
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("{0} is required for {1}").format(
|
||||
get_formula_field_label(row.data_source), row.data_source
|
||||
),
|
||||
message=f"Formula is required for {row.data_source}",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -249,7 +223,7 @@ class DependencyValidator(Validator):
|
||||
cycle = [*path[cycle_start:], node]
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Circular dependency detected: {0}").format(" → ".join(cycle)),
|
||||
message=f"Circular dependency detected: {' → '.join(cycle)}",
|
||||
)
|
||||
)
|
||||
return
|
||||
@@ -281,7 +255,7 @@ class DependencyValidator(Validator):
|
||||
row_idx = self._get_row_idx(ref_code)
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Line References undefined in Formula: {0}").format(", ".join(undefined)),
|
||||
message=f"Line References undefined in Formula: {', '.join(undefined)}",
|
||||
row_idx=row_idx,
|
||||
)
|
||||
)
|
||||
@@ -311,10 +285,9 @@ class CalculationFormulaValidator(Validator):
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("{0} is required for Calculated Amount").format(
|
||||
get_formula_field_label(row.data_source)
|
||||
),
|
||||
message="Formula is required for Calculated Amount",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
return result
|
||||
@@ -326,7 +299,7 @@ class CalculationFormulaValidator(Validator):
|
||||
if not self._are_parentheses_balanced(formula):
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Formula has unbalanced parentheses"),
|
||||
message="Formula has unbalanced parentheses",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -338,7 +311,7 @@ class CalculationFormulaValidator(Validator):
|
||||
if row.reference_code and row.reference_code in refs:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Formula references itself ('{0}')").format(row.reference_code),
|
||||
message=f"Formula references itself ('{row.reference_code}')",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -348,7 +321,7 @@ class CalculationFormulaValidator(Validator):
|
||||
if undefined:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Formula references undefined codes: {0}").format(", ".join(undefined)),
|
||||
message=f"Formula references undefined codes: {', '.join(undefined)}",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -358,7 +331,7 @@ class CalculationFormulaValidator(Validator):
|
||||
if eval_error:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Formula evaluation error: {0}").format(eval_error),
|
||||
message=f"Formula evaluation error: {eval_error}",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -395,7 +368,7 @@ class CalculationFormulaValidator(Validator):
|
||||
result = frappe.safe_eval(formula, eval_globals=None, eval_locals=context)
|
||||
|
||||
if not isinstance(result, (int, float)): # noqa: UP038
|
||||
return _("Formula must return a numeric value, got {0}").format(type(result).__name__)
|
||||
return f"Formula must return a numeric value, got {type(result).__name__}"
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
@@ -418,10 +391,9 @@ class AccountFilterValidator(Validator):
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("{0} is required for Account Data").format(
|
||||
get_formula_field_label(row.data_source)
|
||||
),
|
||||
message="Account filter is required for Account Data",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
return result
|
||||
@@ -437,18 +409,18 @@ class AccountFilterValidator(Validator):
|
||||
if error:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("{0}: {1}").format(get_formula_field_label(row.data_source), error),
|
||||
message=error,
|
||||
row_idx=row.idx,
|
||||
field="Account Filter",
|
||||
)
|
||||
)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("{0}: Invalid JSON format: {1}").format(
|
||||
get_formula_field_label(row.data_source), str(e)
|
||||
),
|
||||
message=f"Invalid JSON format: {e!s}",
|
||||
row_idx=row.idx,
|
||||
field="Account Filter",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -463,38 +435,36 @@ class AccountFilterValidator(Validator):
|
||||
# simple condition: [field, operator, value]
|
||||
if isinstance(filter_config, list):
|
||||
if len(filter_config) != 3:
|
||||
return _("Filter must be [field, operator, value]")
|
||||
return "Filter must be [field, operator, value]"
|
||||
|
||||
field, operator, value = filter_config
|
||||
|
||||
if not isinstance(field, str) or not isinstance(operator, str):
|
||||
return _("Field and operator must be strings")
|
||||
return "Field and operator must be strings"
|
||||
|
||||
display = (
|
||||
field if advanced_filtering else self.account_meta.get_translated_label(field)
|
||||
) or field
|
||||
display = (field if advanced_filtering else self.account_meta.get_label(field)) or field
|
||||
|
||||
if field not in account_fields:
|
||||
return _("Field '{0}' is not a valid Account field").format(display)
|
||||
return f"Field '{display}' is not a valid Account field"
|
||||
|
||||
if operator.casefold() not in OPERATOR_MAP:
|
||||
return _("Invalid operator '{0}'").format(operator)
|
||||
return f"Invalid operator '{operator}'"
|
||||
|
||||
if operator in ["in", "not in"] and not isinstance(value, list):
|
||||
return _("Operator '{0}' requires a list value").format(operator)
|
||||
return f"Operator '{operator}' requires a list value"
|
||||
|
||||
# logical condition: {"and": [condition1, condition2]}
|
||||
elif isinstance(filter_config, dict):
|
||||
if len(filter_config) != 1:
|
||||
return _("Logical condition must have exactly one operator")
|
||||
return "Logical condition must have exactly one operator"
|
||||
|
||||
op = next(iter(filter_config.keys())).lower()
|
||||
if op not in ["and", "or"]:
|
||||
return _("Logical operators must be 'and' or 'or'")
|
||||
return "Logical operators must be 'and' or 'or'"
|
||||
|
||||
conditions = filter_config[next(iter(filter_config.keys()))]
|
||||
if not isinstance(conditions, list) or len(conditions) < 1:
|
||||
return _("Logical conditions need at least 1 sub-condition")
|
||||
return "Logical conditions need at least 1 sub-condition"
|
||||
|
||||
# recursive
|
||||
for condition in conditions:
|
||||
@@ -502,7 +472,7 @@ class AccountFilterValidator(Validator):
|
||||
if error:
|
||||
return error
|
||||
else:
|
||||
return _("Filter must be a list or dict")
|
||||
return "Filter must be a list or dict"
|
||||
|
||||
return None
|
||||
|
||||
@@ -538,31 +508,34 @@ class FormulaValidator(Validator):
|
||||
if "." not in api_path:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("{0} should be in format: app.module.method").format(
|
||||
get_formula_field_label(row.data_source)
|
||||
),
|
||||
message="Custom API path should be in format: app.module.method",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
# Method exists?
|
||||
try:
|
||||
get_valid_api_method(api_path)
|
||||
module_path, method_name = api_path.rsplit(".", 1)
|
||||
module = frappe.get_module(module_path)
|
||||
|
||||
if not hasattr(module, method_name):
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Method '{method_name}' not found in module '{module_path}' (might be environment-specific)",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
if isinstance(e, frappe.PermissionError | frappe.ValidationError):
|
||||
# frappe.throw inside get_valid_api_method logs a message that would pop up in UI
|
||||
frappe.clear_last_message()
|
||||
|
||||
if isinstance(e, frappe.PermissionError):
|
||||
message = _("{0}: Method '{1}' must be whitelisted and permit GET requests").format(
|
||||
get_formula_field_label(row.data_source), api_path
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Could not validate API path: {e!s}",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
else:
|
||||
message = _("Could not validate {0}: {1}").format(
|
||||
get_formula_field_label(row.data_source), str(e)
|
||||
)
|
||||
|
||||
result.add_error(ValidationIssue(message=message, row_idx=row.idx))
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import whitelist_for_tests
|
||||
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_validation import (
|
||||
FormulaValidator,
|
||||
get_valid_api_method,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -77,90 +72,3 @@ class FinancialReportTemplateTestCase(ERPNextTestSuite):
|
||||
{"doctype": "Financial Report Template", "template_name": template_name, "rows": rows_data}
|
||||
)
|
||||
return template
|
||||
|
||||
|
||||
def not_whitelisted_method(**kwargs):
|
||||
return [42.0]
|
||||
|
||||
|
||||
@whitelist_for_tests(methods=["POST"])
|
||||
def whitelisted_post_only_method(**kwargs):
|
||||
return [42.0]
|
||||
|
||||
|
||||
@whitelist_for_tests(methods=["GET"])
|
||||
def whitelisted_get_method(**kwargs):
|
||||
return [42.0]
|
||||
|
||||
|
||||
class TestCustomAPIValidation(FinancialReportTemplateTestCase):
|
||||
"""Custom API rows must point to whitelisted methods that permit GET"""
|
||||
|
||||
TEST_MODULE = "erpnext.accounts.doctype.financial_report_template.test_financial_report_template"
|
||||
NOT_WHITELISTED = f"{TEST_MODULE}.not_whitelisted_method"
|
||||
WHITELISTED_POST_ONLY = f"{TEST_MODULE}.whitelisted_post_only_method"
|
||||
WHITELISTED_GET = f"{TEST_MODULE}.whitelisted_get_method"
|
||||
|
||||
def create_api_template(self, api_path):
|
||||
template = self.create_test_template_with_rows(
|
||||
[
|
||||
{
|
||||
"reference_code": "API001",
|
||||
"display_name": "API Row",
|
||||
"data_source": "Custom API",
|
||||
"calculation_formula": api_path,
|
||||
}
|
||||
]
|
||||
)
|
||||
template.report_type = "Profit and Loss Statement"
|
||||
return template
|
||||
|
||||
def test_get_valid_api_method(self):
|
||||
self.assertRaises(frappe.PermissionError, get_valid_api_method, self.NOT_WHITELISTED)
|
||||
self.assertRaises(frappe.PermissionError, get_valid_api_method, self.WHITELISTED_POST_ONLY)
|
||||
self.assertEqual(get_valid_api_method(self.WHITELISTED_GET), frappe.get_attr(self.WHITELISTED_GET))
|
||||
|
||||
def test_save_rejects_invalid_api_methods(self):
|
||||
for api_path in (self.NOT_WHITELISTED, self.WHITELISTED_POST_ONLY):
|
||||
template = self.create_api_template(api_path)
|
||||
self.assertRaises(frappe.ValidationError, template.insert)
|
||||
|
||||
def test_save_allows_get_whitelisted_method(self):
|
||||
template = self.create_api_template(self.WHITELISTED_GET)
|
||||
template.insert()
|
||||
template.delete()
|
||||
|
||||
def test_engine_rejects_invalid_api_methods(self):
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
ReportContext,
|
||||
RowProcessor,
|
||||
)
|
||||
|
||||
for api_path in (self.NOT_WHITELISTED, self.WHITELISTED_POST_ONLY):
|
||||
template = self.create_api_template(api_path)
|
||||
context = ReportContext(template=template, filters={}, period_list=[{"key": "p1"}])
|
||||
processor = RowProcessor(context)
|
||||
self.assertRaises(frappe.PermissionError, processor._process_api_row, template.rows[0])
|
||||
|
||||
def test_engine_calls_valid_api_method(self):
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
ReportContext,
|
||||
RowProcessor,
|
||||
)
|
||||
|
||||
template = self.create_api_template(self.WHITELISTED_GET)
|
||||
context = ReportContext(template=template, filters={}, period_list=[{"key": "p1"}])
|
||||
processor = RowProcessor(context)
|
||||
row_data = processor._process_api_row(template.rows[0])
|
||||
self.assertEqual(row_data.values, [42.0])
|
||||
|
||||
def test_validation_keeps_message_log_clean(self):
|
||||
validator = FormulaValidator(frappe._dict(rows=[]))
|
||||
message_count = len(frappe.local.message_log)
|
||||
|
||||
# last path raises AppNotInstalledError, which also logs a message via frappe.throw
|
||||
for api_path in (self.NOT_WHITELISTED, self.WHITELISTED_POST_ONLY, "missing_app.api.method"):
|
||||
row = frappe._dict(data_source="Custom API", calculation_formula=api_path, idx=1)
|
||||
result = validator.validate(row)
|
||||
self.assertFalse(result.is_valid)
|
||||
self.assertEqual(len(frappe.local.message_log), message_count)
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:42.509102",
|
||||
"modified": "2024-05-27 17:29:55.560840",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year",
|
||||
@@ -131,15 +131,10 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Auditor"
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "name",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,7 @@ class GLEntry(Document):
|
||||
mandatory = ["account", "voucher_type", "voucher_no", "company"]
|
||||
for k in mandatory:
|
||||
if not self.get(k):
|
||||
frappe.throw(_("{0} is required").format(self.meta.get_translated_label(k)))
|
||||
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
|
||||
|
||||
if not self.is_cancelled and not (self.party_type and self.party):
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:43.571355",
|
||||
"modified": "2024-03-27 13:09:55.573483",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Item Tax Template",
|
||||
@@ -95,61 +95,12 @@
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Item Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,6 @@ def get_data():
|
||||
{"label": _("Pre Sales"), "items": ["Quotation", "Supplier Quotation"]},
|
||||
{"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Delivery Note"]},
|
||||
{"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]},
|
||||
{"label": _("Stock"), "items": ["Item Group", "Item"]},
|
||||
{"label": _("Stock"), "items": ["Item Groups", "Item"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ Object.assign(erpnext.journal_entry, {
|
||||
);
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus == 1 && !frm.doc.reversal_of) {
|
||||
if (frm.doc.docstatus == 1) {
|
||||
frm.add_custom_button(
|
||||
__("Reverse Journal Entry"),
|
||||
() => erpnext.journal_entry.reverse_journal_entry(frm),
|
||||
@@ -677,6 +677,6 @@ Object.assign(erpnext.journal_entry, {
|
||||
} else {
|
||||
erpnext.journal_entry.set_debit_credit_in_company_currency(frm, cdt, cdn);
|
||||
}
|
||||
frm.get_field("accounts").grid.refresh_row(cdn);
|
||||
frm.refresh_field("accounts");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -27,7 +27,6 @@ def get_payment_entry_against_order(
|
||||
) -> dict | Document:
|
||||
"""Build an advance-payment Journal Entry against an unbilled Sales/Purchase Order."""
|
||||
ref_doc = frappe.get_doc(dt, dn)
|
||||
ref_doc.check_permission()
|
||||
|
||||
if flt(ref_doc.per_billed, 2) > 0:
|
||||
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
|
||||
@@ -79,8 +78,6 @@ def get_payment_entry_against_invoice(
|
||||
) -> dict | Document:
|
||||
"""Build a payment Journal Entry against a Sales/Purchase Invoice's outstanding amount."""
|
||||
ref_doc = frappe.get_doc(dt, dn)
|
||||
ref_doc.check_permission()
|
||||
|
||||
if dt == "Sales Invoice":
|
||||
party_type = "Customer"
|
||||
party_account = get_party_account_based_on_invoice_discounting(dn) or ref_doc.debit_to
|
||||
@@ -121,8 +118,6 @@ def get_payment_entry(ref_doc, args: dict) -> dict | Document:
|
||||
Returns the Journal Entry document when `args["journal_entry"]` is truthy, otherwise its
|
||||
dict (for client calls).
|
||||
"""
|
||||
frappe.has_permission("Journal Entry", ptype="create", throw=True)
|
||||
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.update({"voucher_type": "Bank Entry", "company": ref_doc.company, "remark": args.get("remarks")})
|
||||
|
||||
@@ -227,20 +222,6 @@ def make_inter_company_journal_entry(name: str, voucher_type: str, company: str)
|
||||
@frappe.whitelist()
|
||||
def make_reverse_journal_entry(source_name: str, target_doc: str | dict | Document | None = None) -> Document:
|
||||
"""Map a submitted Journal Entry to a reversing one (debits and credits swapped)."""
|
||||
# `get_mapped_doc` checks this as well, but the guards below disclose which entry
|
||||
# reverses which, so read access has to be settled before they run
|
||||
if not frappe.has_permission("Journal Entry", doc=source_name):
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
|
||||
reversal_of = frappe.db.get_value("Journal Entry", source_name, "reversal_of")
|
||||
if reversal_of:
|
||||
frappe.throw(
|
||||
_("{0} is already a Reverse Journal Entry of {1}. Cancel it instead of reversing it.").format(
|
||||
get_link_to_form("Journal Entry", source_name),
|
||||
get_link_to_form("Journal Entry", reversal_of),
|
||||
)
|
||||
)
|
||||
|
||||
existing_reverse = frappe.db.exists("Journal Entry", {"reversal_of": source_name, "docstatus": 1})
|
||||
if existing_reverse:
|
||||
frappe.throw(
|
||||
|
||||
@@ -301,26 +301,6 @@ class TestJournalEntry(ERPNextTestSuite):
|
||||
|
||||
self.check_gl_entries()
|
||||
|
||||
def test_disallow_reversal_of_a_reversal_journal_entry(self):
|
||||
from erpnext.accounts.doctype.journal_entry.mapper import make_reverse_journal_entry
|
||||
|
||||
jv = make_journal_entry("_Test Bank - _TC", "Sales - _TC", 100, submit=True)
|
||||
|
||||
rjv = make_reverse_journal_entry(jv.name)
|
||||
rjv.posting_date = nowdate()
|
||||
rjv.submit()
|
||||
|
||||
self.assertRaisesRegex(
|
||||
frappe.ValidationError,
|
||||
"is already a Reverse Journal Entry",
|
||||
make_reverse_journal_entry,
|
||||
rjv.name,
|
||||
)
|
||||
|
||||
# the guard must not disclose the reversal to a user who cannot read the entry
|
||||
with self.set_user("Guest"):
|
||||
self.assertRaises(frappe.PermissionError, make_reverse_journal_entry, rjv.name)
|
||||
|
||||
def test_disallow_change_in_account_currency_for_a_party(self):
|
||||
# create jv in USD
|
||||
jv = make_journal_entry("_Test Bank USD - _TC", "_Test Receivable USD - _TC", 100, save=False)
|
||||
|
||||
@@ -56,9 +56,7 @@ class LedgerMerge(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def form_start_merge(docname: str):
|
||||
lm_doc = frappe.get_doc("Ledger Merge", docname)
|
||||
lm_doc.check_permission("write")
|
||||
return lm_doc.start_merge()
|
||||
return frappe.get_doc("Ledger Merge", docname).start_merge()
|
||||
|
||||
|
||||
def start_merge(docname):
|
||||
|
||||
@@ -147,14 +147,14 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:44.144864",
|
||||
"modified": "2024-03-27 13:10:03.361383",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Loyalty Program",
|
||||
@@ -171,20 +171,11 @@
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from unittest.mock import patch
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Sum
|
||||
@@ -196,7 +196,7 @@ class TestLoyaltyProgram(ERPNextTestSuite):
|
||||
for d in company_wise_info:
|
||||
self.assertTrue(d.get("loyalty_points"))
|
||||
|
||||
@patch("erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_details")
|
||||
@unittest.mock.patch("erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_details")
|
||||
def test_tier_selection(self, mock_get_loyalty_details):
|
||||
# Create a new loyalty program with multiple tiers
|
||||
loyalty_program = frappe.get_doc(
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:44.763131",
|
||||
"modified": "2026-04-14 18:16:47.795986",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Mode of Payment",
|
||||
@@ -76,30 +76,6 @@
|
||||
{
|
||||
"role": "HR Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"icon": "fa fa-bar-chart",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:44.908490",
|
||||
"modified": "2024-03-27 13:10:05.873547",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Monthly Distribution",
|
||||
@@ -69,14 +69,9 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -297,9 +297,6 @@ def start_import(invoices):
|
||||
invoice_number = d.invoice_number
|
||||
doc = frappe.get_doc(d)
|
||||
doc.flags.ignore_mandatory = True
|
||||
# the outstanding amount is entered inclusive of tax, so taxes must not
|
||||
# be added on top of it
|
||||
doc.flags.dont_auto_add_taxes = True
|
||||
doc.insert(set_name=invoice_number)
|
||||
doc.submit()
|
||||
if not frappe.in_test:
|
||||
|
||||
@@ -4,11 +4,9 @@
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
|
||||
get_temporary_opening_account,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_rule.test_tax_rule import make_tax_rule
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
@@ -128,55 +126,6 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
|
||||
for invoice in invoices:
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", invoice, "department"), "Sales - _TOIC")
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Accounts Settings",
|
||||
{"add_taxes_from_taxes_and_charges_template": 1, "add_taxes_from_item_tax_template": 0},
|
||||
)
|
||||
def test_opening_invoice_creation_without_taxes(self):
|
||||
company = "_Test Opening Invoice Company"
|
||||
template = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Sales Taxes and Charges Template",
|
||||
"company": company,
|
||||
"title": "_Test Opening Invoice Tax",
|
||||
"taxes": [
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": create_account(
|
||||
account_name="_Test Opening Tax Account",
|
||||
parent_account="Duties and Taxes - _TOIC",
|
||||
account_type="Tax",
|
||||
company=company,
|
||||
),
|
||||
"description": "Test taxes",
|
||||
"rate": 9,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# makes the template the default for the party, as it would be on a live site
|
||||
make_tax_rule(tax_type="Sales", company=company, sales_tax_template=template.name, save=1)
|
||||
|
||||
tool = self.make_invoices(company=company, return_doc=True)
|
||||
invoices = tool.make_invoices()
|
||||
self.assertEqual(len(invoices), 2)
|
||||
|
||||
# outstanding amount is entered inclusive of tax, so taxes must not be added on top of it
|
||||
for invoice in invoices:
|
||||
si = frappe.get_doc("Sales Invoice", invoice)
|
||||
self.assertFalse(si.taxes)
|
||||
self.assertEqual(si.grand_total, 200)
|
||||
self.assertEqual(si.outstanding_amount, 200)
|
||||
|
||||
# the same invoice created outside the tool keeps the default taxes,
|
||||
# since adding them there is the user's decision
|
||||
si = frappe.get_doc(tool.get_invoices()[0])
|
||||
si.flags.ignore_mandatory = True
|
||||
si.insert()
|
||||
self.assertTrue(si.taxes)
|
||||
self.assertEqual(si.grand_total, 218)
|
||||
|
||||
def test_opening_entry_project_linking(self):
|
||||
doc = self.make_invoices(
|
||||
company="_Test Opening Invoice Company", invoice_type="Sales", return_doc=True
|
||||
|
||||
@@ -476,8 +476,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.employee_query",
|
||||
};
|
||||
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
|
||||
return erpnext.queries.party(frm.doc);
|
||||
} else if (frm.doc.party_type == "Shareholder") {
|
||||
return {
|
||||
filters: {
|
||||
@@ -1279,14 +1277,8 @@ frappe.ui.form.on("Payment Entry", {
|
||||
await frappe.after_ajax();
|
||||
const base_paid_amount = frm.doc.base_paid_amount || 0;
|
||||
const base_received_amount = frm.doc.base_received_amount || 0;
|
||||
let other_deductions = 0;
|
||||
if (frm.doc.payment_type === "Internal Transfer") {
|
||||
other_deductions = (frm.doc.deductions || [])
|
||||
.filter((row) => !row.is_exchange_gain_loss)
|
||||
.reduce((sum, row) => sum + flt(row.amount), 0);
|
||||
}
|
||||
const exchange_gain_loss = flt(
|
||||
base_paid_amount - base_received_amount - other_deductions,
|
||||
base_paid_amount - base_received_amount,
|
||||
get_deduction_amount_precision()
|
||||
);
|
||||
|
||||
@@ -1863,19 +1855,11 @@ frappe.ui.form.on("Payment Entry Deduction", {
|
||||
},
|
||||
|
||||
amount: function (frm) {
|
||||
if (frm.doc.payment_type === "Internal Transfer") {
|
||||
frm.events.set_exchange_gain_loss_deduction(frm);
|
||||
} else {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
deductions_remove: function (frm) {
|
||||
if (frm.doc.payment_type === "Internal Transfer") {
|
||||
frm.events.set_exchange_gain_loss_deduction(frm);
|
||||
} else {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ from erpnext.accounts.party import (
|
||||
complete_contact_details,
|
||||
get_default_contact,
|
||||
get_party_account,
|
||||
validate_party_company,
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
@@ -278,8 +277,7 @@ class PaymentEntry(AccountsController):
|
||||
if not liability_account:
|
||||
throw(
|
||||
_("Please set default {0} in Company {1}").format(
|
||||
frappe.bold(frappe.get_meta("Company").get_translated_label(fieldname)),
|
||||
frappe.bold(self.company),
|
||||
frappe.bold(frappe.get_meta("Company").get_label(fieldname)), frappe.bold(self.company)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -662,7 +660,7 @@ class PaymentEntry(AccountsController):
|
||||
def validate_mandatory(self):
|
||||
for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"):
|
||||
if not self.get(field):
|
||||
frappe.throw(_("{0} is mandatory").format(self.meta.get_translated_label(field)))
|
||||
frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label(field))))
|
||||
|
||||
def validate_reference_documents(self):
|
||||
valid_reference_doctypes = self.get_valid_reference_doctypes()
|
||||
@@ -1118,14 +1116,8 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
def set_exchange_gain_loss(self):
|
||||
other_deductions = 0
|
||||
if self.payment_type == "Internal Transfer":
|
||||
other_deductions = sum(
|
||||
flt(row.amount) for row in self.get("deductions") if not row.is_exchange_gain_loss
|
||||
)
|
||||
|
||||
exchange_gain_loss = flt(
|
||||
self.base_paid_amount - self.base_received_amount - other_deductions,
|
||||
self.base_paid_amount - self.base_received_amount,
|
||||
self.precision("amount", "deductions"),
|
||||
)
|
||||
|
||||
@@ -1158,7 +1150,7 @@ class PaymentEntry(AccountsController):
|
||||
if value:
|
||||
continue
|
||||
|
||||
label = frappe.get_meta("Company").get_translated_label(fieldname)
|
||||
label = _(frappe.get_meta("Company").get_label(fieldname))
|
||||
return frappe.msgprint(
|
||||
_("Please set {0} in Company {1} to account for Exchange Gain / Loss").format(
|
||||
label, get_link_to_form("Company", self.company)
|
||||
@@ -2442,7 +2434,6 @@ def get_party_details(company: str, party_type: str, party: str, date: str, cost
|
||||
|
||||
ptype = "select" if frappe.only_has_select_perm(party_type) else "read"
|
||||
frappe.has_permission(party_type, ptype, party, throw=True)
|
||||
validate_party_company(party_type, party, company)
|
||||
|
||||
party_account = get_party_account(party_type, party, company)
|
||||
account_currency = get_account_currency(party_account)
|
||||
@@ -2626,11 +2617,7 @@ def get_payment_entry(
|
||||
reference_date: str | date | None = None,
|
||||
created_from_payment_request: bool | None = None,
|
||||
):
|
||||
frappe.has_permission("Payment Entry", ptype="create", throw=True)
|
||||
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
doc.check_permission()
|
||||
|
||||
over_billing_allowance = frappe.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (100.0 + over_billing_allowance):
|
||||
frappe.throw(_("Can only make payment against unbilled {0}").format(_(dt)))
|
||||
|
||||
@@ -789,6 +789,7 @@ class TestPaymentEntry(ERPNextTestSuite):
|
||||
company="_Test Company",
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Company", "bank_charges_account", bank_charges_account)
|
||||
self.addCleanup(frappe.db.set_value, "Company", "_Test Company", "bank_charges_account", "")
|
||||
|
||||
pe = frappe.new_doc("Payment Entry")
|
||||
pe.payment_type = "Internal Transfer"
|
||||
@@ -833,6 +834,7 @@ class TestPaymentEntry(ERPNextTestSuite):
|
||||
company="_Test Company",
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Company", "bank_charges_account", bank_charges_account)
|
||||
self.addCleanup(frappe.db.set_value, "Company", "_Test Company", "bank_charges_account", "")
|
||||
|
||||
pe = frappe.new_doc("Payment Entry")
|
||||
pe.payment_type = "Internal Transfer"
|
||||
@@ -868,64 +870,6 @@ class TestPaymentEntry(ERPNextTestSuite):
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
def test_cross_currency_transfer_splits_bank_charge_and_exchange_gain_loss(self):
|
||||
exchange_gain_loss_account = frappe.db.get_value(
|
||||
"Company", "_Test Company", "exchange_gain_loss_account"
|
||||
)
|
||||
bank_charges_account = create_account(
|
||||
parent_account="Indirect Expenses - _TC",
|
||||
account_name="_Test Bank Charges",
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
pe = frappe.new_doc("Payment Entry")
|
||||
pe.payment_type = "Internal Transfer"
|
||||
pe.company = "_Test Company"
|
||||
pe.paid_from = "_Test Bank USD - _TC"
|
||||
pe.paid_to = "_Test Bank - _TC"
|
||||
pe.paid_amount = 100
|
||||
pe.source_exchange_rate = 50
|
||||
pe.received_amount = 4500
|
||||
pe.reference_no = "6"
|
||||
pe.reference_date = nowdate()
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": bank_charges_account,
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 100,
|
||||
},
|
||||
)
|
||||
|
||||
pe.setup_party_account_field()
|
||||
pe.set_missing_values()
|
||||
pe.set_exchange_rate()
|
||||
pe.set_amounts()
|
||||
|
||||
deductions = {d.account: d for d in pe.deductions}
|
||||
self.assertEqual(deductions[bank_charges_account].amount, 100)
|
||||
self.assertEqual(deductions[exchange_gain_loss_account].amount, 400)
|
||||
self.assertTrue(deductions[exchange_gain_loss_account].is_exchange_gain_loss)
|
||||
self.assertEqual(pe.difference_amount, 0)
|
||||
|
||||
for d in pe.deductions:
|
||||
d.cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
["_Test Bank USD - _TC", 0, 5000, None],
|
||||
["_Test Bank - _TC", 4500, 0, None],
|
||||
[exchange_gain_loss_account, 400.0, 0, None],
|
||||
[bank_charges_account, 100.0, 0, None],
|
||||
]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
def test_payment_against_negative_sales_invoice(self):
|
||||
si1 = create_sales_invoice()
|
||||
|
||||
@@ -1107,6 +1051,8 @@ class TestPaymentEntry(ERPNextTestSuite):
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Company", "exchange_gain_account", gain_account)
|
||||
frappe.db.set_value("Company", "_Test Company", "exchange_loss_account", loss_account)
|
||||
self.addCleanup(frappe.db.set_value, "Company", "_Test Company", "exchange_gain_account", "")
|
||||
self.addCleanup(frappe.db.set_value, "Company", "_Test Company", "exchange_loss_account", "")
|
||||
|
||||
si_gain = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
|
||||
@@ -92,7 +92,6 @@ def get_supplier_query(doctype: str, txt: str, searchfield: str, start: int, pag
|
||||
@frappe.whitelist()
|
||||
def make_payment_records(name: str, supplier: str, mode_of_payment: str | None = None):
|
||||
doc = frappe.get_doc("Payment Order", name)
|
||||
doc.check_permission()
|
||||
make_journal_entry(doc, supplier, mode_of_payment)
|
||||
|
||||
|
||||
|
||||
@@ -647,7 +647,7 @@ class PaymentReconciliation(Document):
|
||||
def check_mandatory_to_fetch(self):
|
||||
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
|
||||
if not self.get(fieldname):
|
||||
frappe.throw(_("Please select {0} first").format(self.meta.get_translated_label(fieldname)))
|
||||
frappe.throw(_("Please select {0} first").format(_(self.meta.get_label(fieldname))))
|
||||
|
||||
def validate_entries(self):
|
||||
if not self.get("invoices"):
|
||||
|
||||
@@ -201,6 +201,8 @@ class TestPaymentReconciliation(ERPNextTestSuite):
|
||||
)
|
||||
frappe.db.set_value("Company", self.company, "exchange_gain_account", gain_account)
|
||||
frappe.db.set_value("Company", self.company, "exchange_loss_account", loss_account)
|
||||
self.addCleanup(frappe.db.set_value, "Company", self.company, "exchange_gain_account", "")
|
||||
self.addCleanup(frappe.db.set_value, "Company", self.company, "exchange_loss_account", "")
|
||||
return gain_account, loss_account
|
||||
|
||||
def create_foreign_currency_sales_invoice(self, conversion_rate):
|
||||
@@ -1329,6 +1331,15 @@ class TestPaymentReconciliation(ERPNextTestSuite):
|
||||
test_user = "test@example.com"
|
||||
permitted_ccs = ["_Test Cost Center - _TC", "_Test Cost Center 2 - _TC"]
|
||||
restricted_cc = "_Test Write Off Cost Center - _TC"
|
||||
existing_apply_strict_user_permissions = cint(
|
||||
frappe.db.get_single_value("System Settings", "apply_strict_user_permissions")
|
||||
)
|
||||
self.addCleanup(
|
||||
frappe.db.set_single_value,
|
||||
"System Settings",
|
||||
"apply_strict_user_permissions",
|
||||
existing_apply_strict_user_permissions,
|
||||
)
|
||||
transaction_date = nowdate()
|
||||
rate = 100
|
||||
|
||||
|
||||
@@ -92,7 +92,6 @@ frappe.ui.form.on("Payment Request", "is_a_subscription", function (frm) {
|
||||
freeze: true,
|
||||
callback: function (data) {
|
||||
if (!data.exc) {
|
||||
frm.clear_table("subscription_plans");
|
||||
$.each(data.message || [], function (i, v) {
|
||||
var d = frappe.model.add_child(
|
||||
frm.doc,
|
||||
|
||||
@@ -875,7 +875,6 @@ def make_payment_request(**args):
|
||||
party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company)
|
||||
party_account_currency = get_account_currency(party_account)
|
||||
|
||||
subscription_plans = get_subscription_details(ref_doc.doctype, ref_doc.name)
|
||||
pr.update(
|
||||
{
|
||||
"payment_gateway_account": gateway_account.get("name"),
|
||||
@@ -907,24 +906,12 @@ def make_payment_request(**args):
|
||||
or gateway_account.get("payment_channel", "Email") != "Email"
|
||||
),
|
||||
"phone_number": args.get("phone_number") if args.get("phone_number") else None,
|
||||
"is_a_subscription": 1 if subscription_plans else 0,
|
||||
}
|
||||
)
|
||||
|
||||
if selected_payment_schedules:
|
||||
apply_payment_references(pr, payment_reference)
|
||||
|
||||
if subscription_plans:
|
||||
pr.set(
|
||||
"subscription_plans",
|
||||
[
|
||||
{
|
||||
"plan": row.plan,
|
||||
"qty": row.qty,
|
||||
}
|
||||
for row in subscription_plans
|
||||
],
|
||||
)
|
||||
# Dimensions
|
||||
pr.update(
|
||||
{
|
||||
@@ -1238,25 +1225,20 @@ def get_dummy_message(doc):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_subscription_details(reference_doctype: str, reference_name: str) -> list[dict]:
|
||||
frappe.has_permission(reference_doctype, "read", reference_name, throw=True)
|
||||
|
||||
if not frappe.get_meta(reference_doctype).has_field("subscription"):
|
||||
return []
|
||||
|
||||
subscription = frappe.db.get_value(reference_doctype, reference_name, "subscription")
|
||||
|
||||
if not subscription:
|
||||
return []
|
||||
|
||||
return frappe.get_all(
|
||||
"Subscription Plan Detail",
|
||||
filters={"parent": subscription, "parenttype": "Subscription", "parentfield": "plans"},
|
||||
fields=[
|
||||
"plan",
|
||||
"qty",
|
||||
],
|
||||
)
|
||||
def get_subscription_details(reference_doctype: str, reference_name: str):
|
||||
if reference_doctype == "Sales Invoice":
|
||||
subscriptions = frappe.get_all(
|
||||
"Subscription Invoice",
|
||||
filters={"invoice": reference_name},
|
||||
fields=["parent as sub_name"],
|
||||
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
|
||||
)
|
||||
subscription_plans = []
|
||||
for subscription in subscriptions:
|
||||
plans = frappe.get_doc("Subscription", subscription.sub_name).plans
|
||||
for plan in plans:
|
||||
subscription_plans.append(plan)
|
||||
return subscription_plans
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -1359,7 +1341,6 @@ def get_irequests_of_payment_request(doc: str | None = None) -> list:
|
||||
@frappe.whitelist()
|
||||
def get_available_payment_schedules(reference_doctype: str, reference_name: str):
|
||||
ref_doc = frappe.get_doc(reference_doctype, reference_name)
|
||||
ref_doc.check_permission()
|
||||
|
||||
if not hasattr(ref_doc, "payment_schedule") or not ref_doc.payment_schedule:
|
||||
return []
|
||||
|
||||
@@ -11,27 +11,15 @@ from frappe.utils import add_days, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import (
|
||||
get_subscription_details,
|
||||
make_payment_request,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.doctype.subscription.test_subscription import (
|
||||
create_plan,
|
||||
create_subscription,
|
||||
make_plans,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
PAYMENT_URL = "https://example.com/payment"
|
||||
SEND_EMAIL_MOCK = MagicMock(return_value=None)
|
||||
GET_PAYMENT_URL_MOCK = MagicMock(return_value=PAYMENT_URL)
|
||||
GET_PAYMENT_GATEWAY_CONTROLLER_MOCK = MagicMock()
|
||||
|
||||
payment_gateways = [
|
||||
{"doctype": "Payment Gateway", "gateway": "_Test Gateway"},
|
||||
@@ -74,18 +62,6 @@ payment_method = [
|
||||
]
|
||||
|
||||
|
||||
@patch(
|
||||
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email",
|
||||
new=SEND_EMAIL_MOCK,
|
||||
)
|
||||
@patch(
|
||||
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
|
||||
new=GET_PAYMENT_URL_MOCK,
|
||||
)
|
||||
@patch(
|
||||
"erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller",
|
||||
new=GET_PAYMENT_GATEWAY_CONTROLLER_MOCK,
|
||||
)
|
||||
class TestPaymentRequest(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
for payment_gateway in payment_gateways:
|
||||
@@ -104,11 +80,24 @@ class TestPaymentRequest(ERPNextTestSuite):
|
||||
):
|
||||
frappe.get_doc(method).insert(ignore_permissions=True)
|
||||
|
||||
for mock in (SEND_EMAIL_MOCK, GET_PAYMENT_URL_MOCK, GET_PAYMENT_GATEWAY_CONTROLLER_MOCK):
|
||||
mock.reset_mock()
|
||||
self.send_email = SEND_EMAIL_MOCK
|
||||
self.get_payment_url = GET_PAYMENT_URL_MOCK
|
||||
self._get_payment_gateway_controller = GET_PAYMENT_GATEWAY_CONTROLLER_MOCK
|
||||
send_email = patch(
|
||||
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email",
|
||||
return_value=None,
|
||||
)
|
||||
self.send_email = send_email.start()
|
||||
self.addCleanup(send_email.stop)
|
||||
get_payment_url = patch(
|
||||
# this also shadows one (1) call to _get_payment_gateway_controller
|
||||
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
|
||||
return_value=PAYMENT_URL,
|
||||
)
|
||||
self.get_payment_url = get_payment_url.start()
|
||||
self.addCleanup(get_payment_url.stop)
|
||||
_get_payment_gateway_controller = patch(
|
||||
"erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller",
|
||||
)
|
||||
self._get_payment_gateway_controller = _get_payment_gateway_controller.start()
|
||||
self.addCleanup(_get_payment_gateway_controller.stop)
|
||||
|
||||
def test_payment_request_linkings(self):
|
||||
so_inr = make_sales_order(currency="INR", do_not_save=True)
|
||||
@@ -2020,140 +2009,3 @@ class TestPaymentRequestV2Gateway(ERPNextTestSuite):
|
||||
call_kwargs = mock_log_error.call_args
|
||||
self.assertIn("Payment Initialization Failed", str(call_kwargs))
|
||||
self.assertIn("_Test Gateway", str(call_kwargs))
|
||||
|
||||
def test_payment_request_with_subscription(self):
|
||||
make_plans()
|
||||
|
||||
subscription_plan = frappe.get_doc("Subscription Plan", "_Test Plan Name")
|
||||
subscription_plan.payment_gateway = "_Test Gateway - INR - _TC"
|
||||
subscription_plan.save()
|
||||
|
||||
subscription = create_subscription(
|
||||
plans=[{"plan": "_Test Plan Name", "qty": 1}],
|
||||
start_date=nowdate(),
|
||||
generate_invoice_at="Prepaid (bill at period start)",
|
||||
submit_invoice=1,
|
||||
)
|
||||
invoice_name = frappe.get_value(
|
||||
"Sales Invoice",
|
||||
{
|
||||
"subscription": subscription.name,
|
||||
"docstatus": 1,
|
||||
"is_return": 0,
|
||||
},
|
||||
"name",
|
||||
order_by="from_date asc",
|
||||
)
|
||||
|
||||
payment_request = make_payment_request(
|
||||
dt="Sales Invoice",
|
||||
dn=invoice_name,
|
||||
recipient_id="test@example.com",
|
||||
)
|
||||
|
||||
self.assertEqual(payment_request.is_a_subscription, 1)
|
||||
self.assertEqual(len(payment_request.subscription_plans), 1)
|
||||
|
||||
subscription_plan = payment_request.subscription_plans[0]
|
||||
self.assertEqual(subscription_plan.plan, "_Test Plan Name")
|
||||
self.assertEqual(subscription_plan.qty, 1)
|
||||
self.assertEqual(payment_request.reference_doctype, "Sales Invoice")
|
||||
self.assertEqual(payment_request.reference_name, invoice_name)
|
||||
|
||||
def test_payment_request_without_subscription(self):
|
||||
si = create_sales_invoice()
|
||||
payment_request = make_payment_request(
|
||||
dt="Sales Invoice",
|
||||
dn=si.name,
|
||||
recipient_id="test@example.com",
|
||||
)
|
||||
self.assertEqual(payment_request.is_a_subscription, 0)
|
||||
self.assertEqual(len(payment_request.subscription_plans), 0)
|
||||
self.assertEqual(payment_request.reference_doctype, "Sales Invoice")
|
||||
self.assertEqual(payment_request.reference_name, si.name)
|
||||
|
||||
def test_payment_request_with_subscription_for_purchase_invoice(self):
|
||||
make_plans()
|
||||
|
||||
subscription_plan = frappe.get_doc("Subscription Plan", "_Test Plan Name")
|
||||
subscription_plan.payment_gateway = "_Test Gateway - INR - _TC"
|
||||
subscription_plan.save()
|
||||
|
||||
subscription = create_subscription(
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier",
|
||||
plans=[{"plan": "_Test Plan Name", "qty": 1}],
|
||||
start_date=nowdate(),
|
||||
generate_invoice_at="Prepaid (bill at period start)",
|
||||
submit_invoice=1,
|
||||
)
|
||||
invoice_name = frappe.get_value(
|
||||
"Purchase Invoice",
|
||||
{
|
||||
"subscription": subscription.name,
|
||||
"docstatus": 1,
|
||||
"is_return": 0,
|
||||
},
|
||||
"name",
|
||||
order_by="from_date asc",
|
||||
)
|
||||
|
||||
payment_request = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
dn=invoice_name,
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier",
|
||||
recipient_id="test@example.com",
|
||||
)
|
||||
|
||||
self.assertEqual(payment_request.is_a_subscription, 1)
|
||||
self.assertEqual(len(payment_request.subscription_plans), 1)
|
||||
|
||||
subscription_plan = payment_request.subscription_plans[0]
|
||||
self.assertEqual(subscription_plan.plan, "_Test Plan Name")
|
||||
self.assertEqual(subscription_plan.qty, 1)
|
||||
self.assertEqual(payment_request.reference_doctype, "Purchase Invoice")
|
||||
self.assertEqual(payment_request.reference_name, invoice_name)
|
||||
|
||||
def test_payment_request_without_subscription_for_purchase_invoice(self):
|
||||
pi = make_purchase_invoice()
|
||||
payment_request = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
dn=pi.name,
|
||||
party_type="Supplier",
|
||||
party=pi.supplier,
|
||||
recipient_id="test@example.com",
|
||||
)
|
||||
self.assertEqual(payment_request.is_a_subscription, 0)
|
||||
self.assertEqual(len(payment_request.subscription_plans), 0)
|
||||
self.assertEqual(payment_request.reference_doctype, "Purchase Invoice")
|
||||
self.assertEqual(payment_request.reference_name, pi.name)
|
||||
|
||||
def test_get_subscription_details_returns_empty_for_doctype_without_subscription_field(self):
|
||||
so = make_sales_order()
|
||||
self.assertEqual(get_subscription_details("Sales Order", so.name), [])
|
||||
|
||||
def test_get_subscription_details_requires_read_permission_on_reference(self):
|
||||
si = create_sales_invoice()
|
||||
|
||||
restricted_user = "no-roles@example.com"
|
||||
if not frappe.db.exists("User", restricted_user):
|
||||
user = frappe.new_doc("User")
|
||||
user.email = restricted_user
|
||||
user.first_name = "No Roles"
|
||||
user.send_welcome_email = 0
|
||||
user.insert()
|
||||
|
||||
accounts_user = "accounts-user@example.com"
|
||||
if not frappe.db.exists("User", accounts_user):
|
||||
user = frappe.new_doc("User")
|
||||
user.email = accounts_user
|
||||
user.first_name = "Accounts"
|
||||
user.send_welcome_email = 0
|
||||
user.add_roles("Accounts User")
|
||||
|
||||
with self.set_user(restricted_user):
|
||||
self.assertRaises(frappe.PermissionError, get_subscription_details, "Sales Invoice", si.name)
|
||||
|
||||
with self.set_user(accounts_user):
|
||||
self.assertEqual(get_subscription_details("Sales Invoice", si.name), [])
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:45.693762",
|
||||
"modified": "2024-03-27 13:10:11.511137",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Term",
|
||||
@@ -157,36 +157,11 @@
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -263,15 +263,12 @@ def get_cashiers(doctype: str, txt: str, searchfield: str, start: int, page_len:
|
||||
@frappe.whitelist()
|
||||
def get_invoices(start: str | datetime, end: str | datetime, pos_profile: str, user: str):
|
||||
invoice_doctype = frappe.db.get_single_value("POS Settings", "invoice_type")
|
||||
frappe.has_permission("POS Profile", doc=pos_profile, throw=True)
|
||||
|
||||
frappe.has_permission("Sales Invoice", throw=True)
|
||||
sales_inv_query = build_invoice_query("Sales Invoice", user, pos_profile, start, end)
|
||||
|
||||
query = sales_inv_query
|
||||
|
||||
if invoice_doctype == "POS Invoice":
|
||||
frappe.has_permission("POS Invoice", throw=True)
|
||||
pos_inv_query = build_invoice_query("POS Invoice", user, pos_profile, start, end)
|
||||
query = query + pos_inv_query
|
||||
|
||||
|
||||
@@ -21,12 +21,13 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.test_user, self.pos_profile = init_user_and_profile()
|
||||
init_user_and_profile()
|
||||
make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100)
|
||||
frappe.db.set_single_value("POS Settings", "invoice_type", "POS Invoice")
|
||||
|
||||
def test_pos_closing_entry(self):
|
||||
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
@@ -58,7 +59,8 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
"""
|
||||
Test if POS Closing Entry is created without item code
|
||||
"""
|
||||
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv = create_pos_invoice(rate=3500, do_not_submit=1, item_name="Test Item", without_item_code=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
@@ -77,9 +79,10 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
"""
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
|
||||
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
test_item_qty = get_test_item_qty(self.pos_profile)
|
||||
test_item_qty = get_test_item_qty(pos_profile)
|
||||
|
||||
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
@@ -101,11 +104,13 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
pcv_doc.flags.in_test = True
|
||||
pcv_doc.submit()
|
||||
|
||||
test_item_qty_after_sales = get_test_item_qty(self.pos_profile)
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
test_item_qty_after_sales = get_test_item_qty(pos_profile)
|
||||
self.assertEqual(test_item_qty_after_sales, test_item_qty - 1)
|
||||
|
||||
def test_cancelling_of_pos_closing_entry(self):
|
||||
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
@@ -164,7 +169,9 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
pos_profile.insert()
|
||||
self.assertTrue(frappe.db.exists("POS Profile", pos_profile.name))
|
||||
|
||||
opening_entry = create_opening_entry(pos_profile, self.test_user.name)
|
||||
test_user = init_user_and_profile(do_not_create_pos_profile=1)
|
||||
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
pos_inv1 = create_pos_invoice(rate=350, do_not_submit=1, pos_profile=pos_profile.name)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv1.save()
|
||||
@@ -188,6 +195,9 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
|
||||
def test_merging_into_sales_invoice_for_batched_item(self):
|
||||
frappe.flags.print_message = False
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import (
|
||||
init_user_and_profile,
|
||||
)
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
item_doc = make_item(
|
||||
@@ -210,7 +220,8 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
)
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv = create_pos_invoice(
|
||||
item_code=item_code,
|
||||
@@ -280,17 +291,18 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
|
||||
@ERPNextTestSuite.change_settings("POS Settings", {"invoice_type": "Sales Invoice"})
|
||||
def test_closing_entries_with_sales_invoice(self):
|
||||
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_si = create_sales_invoice(
|
||||
qty=10, is_created_using_pos=1, pos_profile=self.pos_profile.name, do_not_save=1
|
||||
qty=10, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=1
|
||||
)
|
||||
pos_si.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
|
||||
pos_si.save()
|
||||
pos_si.submit()
|
||||
|
||||
pos_si2 = create_sales_invoice(
|
||||
qty=5, is_created_using_pos=1, pos_profile=self.pos_profile.name, do_not_save=11
|
||||
qty=5, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=11
|
||||
)
|
||||
pos_si2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
|
||||
pos_si2.save()
|
||||
@@ -320,10 +332,12 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_sales_return
|
||||
|
||||
with self.change_settings("POS Settings", {"invoice_type": "Sales Invoice"}):
|
||||
opening_entry1 = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
|
||||
pos_si1, pos_si2 = create_multiple_sales_invoices(self.pos_profile)
|
||||
with self.change_settings("POS Settings", {"invoice_type": "Sales Invoice"}):
|
||||
opening_entry1 = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_si1, pos_si2 = create_multiple_sales_invoices(pos_profile)
|
||||
|
||||
pos_inv = create_pos_invoice(rate=100, do_not_save=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
|
||||
@@ -343,13 +357,13 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
self.assertEqual(pos_si2.pos_closing_entry, pcv_doc1.name)
|
||||
|
||||
with self.change_settings("POS Settings", {"invoice_type": "POS Invoice"}):
|
||||
opening_entry2 = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
opening_entry2 = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv1, pos_inv2 = create_multiple_pos_invoices(self.pos_profile)
|
||||
pos_inv1, pos_inv2 = create_multiple_pos_invoices(pos_profile)
|
||||
|
||||
# Trying to create Sales Invoice when invoice_type is set to POS Invoice.
|
||||
pos_si3 = create_sales_invoice(
|
||||
qty=1, is_created_using_pos=1, pos_profile=self.pos_profile.name, do_not_save=1
|
||||
qty=1, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=1
|
||||
)
|
||||
pos_si3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
|
||||
self.assertRaises(frappe.ValidationError, pos_si3.save)
|
||||
@@ -380,14 +394,16 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
"""
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
|
||||
with self.change_settings("POS Settings", {"invoice_type": "POS Invoice"}):
|
||||
opening_entry1 = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
|
||||
pos_inv1, pos_inv2 = create_multiple_pos_invoices(self.pos_profile)
|
||||
with self.change_settings("POS Settings", {"invoice_type": "POS Invoice"}):
|
||||
opening_entry1 = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv1, pos_inv2 = create_multiple_pos_invoices(pos_profile)
|
||||
|
||||
# Trying to create Sales Invoice when invoice_type is set to POS Invoice.
|
||||
pos_sinv = create_sales_invoice(
|
||||
qty=1, is_created_using_pos=1, pos_profile=self.pos_profile.name, do_not_save=1
|
||||
qty=1, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=1
|
||||
)
|
||||
pos_sinv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
|
||||
self.assertRaises(frappe.ValidationError, pos_sinv.save)
|
||||
@@ -405,9 +421,9 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
self.assertEqual(pcv_doc1.grand_total, 300)
|
||||
|
||||
with self.change_settings("POS Settings", {"invoice_type": "Sales Invoice"}):
|
||||
opening_entry2 = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
opening_entry2 = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_si1, pos_si2 = create_multiple_sales_invoices(self.pos_profile)
|
||||
pos_si1, pos_si2 = create_multiple_sales_invoices(pos_profile)
|
||||
|
||||
pos_inv3 = create_pos_invoice(rate=100, do_not_save=1)
|
||||
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
|
||||
|
||||
@@ -497,7 +497,6 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:doc.currency && doc.currency != erpnext.get_currency(doc.company)",
|
||||
"depends_on": "customer",
|
||||
"fieldname": "currency_and_price_list",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -1643,7 +1642,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:45.029925",
|
||||
"modified": "2026-06-21 12:46:13.250145",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
@@ -1686,14 +1685,6 @@
|
||||
"permlevel": 1,
|
||||
"read": 1,
|
||||
"role": "All"
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -4,7 +4,6 @@ import copy
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_to_date
|
||||
|
||||
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
|
||||
set_default_account_for_mode_of_payment,
|
||||
@@ -54,14 +53,14 @@ class TestPOSInvoice(POSInvoiceTestMixin):
|
||||
|
||||
w2 = frappe.get_doc(w.doctype, w.name)
|
||||
|
||||
import time
|
||||
|
||||
time.sleep(1)
|
||||
w.save()
|
||||
frappe.db.set_value(
|
||||
w.doctype,
|
||||
w.name,
|
||||
"modified",
|
||||
add_to_date(w.modified, seconds=1),
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
import time
|
||||
|
||||
time.sleep(1)
|
||||
self.assertRaises(frappe.TimestampMismatchError, w2.save)
|
||||
|
||||
def test_change_naming_series(self):
|
||||
@@ -903,6 +902,9 @@ class TestPOSInvoice(POSInvoiceTestMixin):
|
||||
self.assertEqual(pos_inv.items[0].rate, 300)
|
||||
|
||||
def test_delivered_serial_no_case(self):
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
|
||||
init_user_and_profile,
|
||||
)
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
|
||||
@@ -914,6 +916,8 @@ class TestPOSInvoice(POSInvoiceTestMixin):
|
||||
|
||||
self.assertEqual(serial_no, delivered_serial_no)
|
||||
|
||||
init_user_and_profile()
|
||||
|
||||
pos_inv = create_pos_invoice(
|
||||
item_code="_Test Serialized Item With Series",
|
||||
serial_no=[serial_no],
|
||||
@@ -927,9 +931,13 @@ class TestPOSInvoice(POSInvoiceTestMixin):
|
||||
|
||||
def test_bundle_stock_availability_validation(self):
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import ProductBundleStockValidationError
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
|
||||
init_user_and_profile,
|
||||
)
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.utils import get_stock_balance
|
||||
|
||||
init_user_and_profile()
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -951,18 +959,9 @@ class TestPOSInvoice(POSInvoiceTestMixin):
|
||||
is_stock_item=1,
|
||||
)
|
||||
|
||||
# Set initial stock to SubA=5 and SubB=2, even when this test is rerun on the same site.
|
||||
for item_code, target_qty in ((sub_item_a, 5), (sub_item_b, 2)):
|
||||
balance = get_stock_balance(item_code, warehouse)
|
||||
difference = target_qty - balance
|
||||
if difference:
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
to_warehouse=warehouse if difference > 0 else None,
|
||||
from_warehouse=warehouse if difference < 0 else None,
|
||||
qty=abs(difference),
|
||||
company=company,
|
||||
)
|
||||
# Add initial stock: SubA=5, SubB=2
|
||||
make_stock_entry(item_code=sub_item_a, target=warehouse, qty=5, company=company)
|
||||
make_stock_entry(item_code=sub_item_b, target=warehouse, qty=2, company=company)
|
||||
|
||||
# Create Product Bundle: Test Bundle (SubA x2 + SubB x1)
|
||||
bundle_item = "_Test Bundle"
|
||||
@@ -1011,19 +1010,16 @@ class TestPOSInvoice(POSInvoiceTestMixin):
|
||||
|
||||
def create_pos_invoice(**args):
|
||||
args = frappe._dict(args)
|
||||
pos_profile_name = args.pos_profile
|
||||
if not pos_profile_name:
|
||||
pos_profile_name = frappe.db.exists("POS Profile", "_Test POS Profile")
|
||||
if not pos_profile_name:
|
||||
pos_profile = make_pos_profile()
|
||||
pos_profile.save()
|
||||
pos_profile_name = pos_profile.name
|
||||
pos_profile = None
|
||||
if not args.pos_profile:
|
||||
pos_profile = make_pos_profile()
|
||||
pos_profile.save()
|
||||
|
||||
pos_inv = frappe.new_doc("POS Invoice")
|
||||
pos_inv.update(args)
|
||||
pos_inv.update_stock = 1
|
||||
pos_inv.is_pos = 1
|
||||
pos_inv.pos_profile = pos_profile_name
|
||||
pos_inv.pos_profile = args.pos_profile or pos_profile.name
|
||||
|
||||
if args.posting_date:
|
||||
pos_inv.set_posting_time = 1
|
||||
|
||||
@@ -26,10 +26,14 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
|
||||
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
|
||||
make_closing_entry_from_opening,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import (
|
||||
init_user_and_profile,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
|
||||
consolidate_pos_invoices,
|
||||
)
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 270})
|
||||
pos_inv.save()
|
||||
@@ -51,10 +55,14 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
|
||||
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
|
||||
make_closing_entry_from_opening,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import (
|
||||
init_user_and_profile,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
|
||||
consolidate_pos_invoices,
|
||||
)
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
|
||||
pos_inv.append(
|
||||
@@ -99,6 +107,9 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
|
||||
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
|
||||
make_closing_entry_from_opening,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import (
|
||||
init_user_and_profile,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
|
||||
consolidate_pos_invoices,
|
||||
)
|
||||
@@ -110,6 +121,7 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
|
||||
make_item(item, {"is_stock_item": 1})
|
||||
make_purchase_receipt(item_code=item, warehouse="_Test Warehouse - _TC", qty=1, rate=300)
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
|
||||
pos_inv.append(
|
||||
|
||||
@@ -582,7 +582,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2026-08-21 23:11:45.419667",
|
||||
"modified": "2026-02-10 14:24:48.597412",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
@@ -606,10 +606,6 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -289,11 +289,6 @@ def pos_profile_query(doctype: str, txt: str, searchfield: str, start: int, page
|
||||
user = frappe.session["user"]
|
||||
company = filters.get("company") or frappe.defaults.get_user_default("company")
|
||||
|
||||
allowed_pos_profiles = frappe.get_list("POS Profile", pluck="name")
|
||||
|
||||
if not allowed_pos_profiles:
|
||||
return {}
|
||||
|
||||
pf = frappe.qb.DocType("POS Profile")
|
||||
pfu = frappe.qb.DocType("POS Profile User")
|
||||
|
||||
@@ -303,7 +298,6 @@ def pos_profile_query(doctype: str, txt: str, searchfield: str, start: int, page
|
||||
.on(pfu.parent == pf.name)
|
||||
.select(pf.name)
|
||||
.where((pfu.user == user) & (pf.company == company) & pf.name.like(f"%{txt}%") & (pf.disabled == 0))
|
||||
.where(pf.name.isin(allowed_pos_profiles))
|
||||
.limit(page_len)
|
||||
.offset(start)
|
||||
.run()
|
||||
@@ -320,7 +314,6 @@ def pos_profile_query(doctype: str, txt: str, searchfield: str, start: int, page
|
||||
& (pf.company == company)
|
||||
& pf.name.like(f"%{txt}%")
|
||||
& (pf.disabled == 0)
|
||||
& (pf.name.isin(allowed_pos_profiles))
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
@@ -12,9 +12,8 @@
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Fieldname",
|
||||
"read_only": 1
|
||||
"hidden": 1,
|
||||
"label": "Fieldname"
|
||||
},
|
||||
{
|
||||
"fieldname": "field",
|
||||
@@ -27,7 +26,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-31 20:41:12.000000",
|
||||
"modified": "2025-07-29 18:08:40.323579",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Search Fields",
|
||||
|
||||
@@ -1,9 +1,40 @@
|
||||
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
function is_valid_invoice_field(df) {
|
||||
return frappe.model.no_value_type.indexOf(df.fieldtype) === -1 || df.fieldtype === "Button";
|
||||
}
|
||||
let search_fields_datatypes = [
|
||||
"Data",
|
||||
"Link",
|
||||
"Dynamic Link",
|
||||
"Long Text",
|
||||
"Select",
|
||||
"Small Text",
|
||||
"Text",
|
||||
"Text Editor",
|
||||
];
|
||||
let do_not_include_fields = [
|
||||
"naming_series",
|
||||
"item_code",
|
||||
"item_name",
|
||||
"stock_uom",
|
||||
"asset_naming_series",
|
||||
"default_material_request_type",
|
||||
"valuation_method",
|
||||
"warranty_period",
|
||||
"weight_uom",
|
||||
"batch_number_series",
|
||||
"serial_no_series",
|
||||
"purchase_uom",
|
||||
"customs_tariff_number",
|
||||
"sales_uom",
|
||||
"deferred_revenue_account",
|
||||
"deferred_expense_account",
|
||||
"quality_inspection_template",
|
||||
"route",
|
||||
"slideshow",
|
||||
"website_image_alt",
|
||||
"thumbnail",
|
||||
"web_long_description",
|
||||
];
|
||||
|
||||
frappe.ui.form.on("POS Settings", {
|
||||
onload: function (frm) {
|
||||
@@ -11,97 +42,74 @@ frappe.ui.form.on("POS Settings", {
|
||||
frm.trigger("add_search_options");
|
||||
},
|
||||
|
||||
invoice_type: function (frm) {
|
||||
frm.trigger("get_invoice_fields");
|
||||
},
|
||||
|
||||
get_invoice_fields: function (frm) {
|
||||
const invoice_type = frm.doc.invoice_type;
|
||||
if (!invoice_type) return;
|
||||
|
||||
frappe.model.with_doctype(invoice_type, () => {
|
||||
// the invoice type can change again while the meta loads
|
||||
if (frm.doc.invoice_type !== invoice_type) return;
|
||||
|
||||
const fields = frappe.get_doc("DocType", invoice_type).fields.filter(is_valid_invoice_field);
|
||||
frappe.model.with_doctype("POS Invoice", () => {
|
||||
var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function (d) {
|
||||
if (
|
||||
frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
|
||||
["Button"].includes(d.fieldtype)
|
||||
) {
|
||||
return { label: d.label + " (" + d.fieldtype + ")", value: d.fieldname };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
frm.fields_dict.invoice_fields.grid.update_docfield_property(
|
||||
"fieldname",
|
||||
"options",
|
||||
[""].concat(
|
||||
fields.map((df) => {
|
||||
return { label: `${df.label} (${df.fieldtype})`, value: df.fieldname };
|
||||
})
|
||||
)
|
||||
[""].concat(fields)
|
||||
);
|
||||
|
||||
frm.trigger("validate_invoice_fields");
|
||||
});
|
||||
},
|
||||
|
||||
validate_invoice_fields: function (frm) {
|
||||
const valid_fieldnames = frappe
|
||||
.get_doc("DocType", frm.doc.invoice_type)
|
||||
.fields.filter(is_valid_invoice_field)
|
||||
.map((df) => df.fieldname);
|
||||
|
||||
const invalid_fields = (frm.doc.invoice_fields || [])
|
||||
.filter((row) => row.fieldname && !valid_fieldnames.includes(row.fieldname))
|
||||
.map((row) => `#${row.idx} ${row.fieldname}`);
|
||||
|
||||
if (!invalid_fields.length) return;
|
||||
|
||||
frappe.msgprint({
|
||||
title: __("Invalid POS Fields"),
|
||||
indicator: "orange",
|
||||
message: __("The following rows are not valid fields of {0} and have to be removed: {1}", [
|
||||
frm.doc.invoice_type.bold(),
|
||||
invalid_fields.join(", "),
|
||||
]),
|
||||
});
|
||||
},
|
||||
|
||||
add_search_options: function (frm) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.pos_settings.pos_settings.get_pos_search_field_options",
|
||||
callback: ({ message }) => {
|
||||
const fields = message || [];
|
||||
frappe.model.with_doctype("Item", () => {
|
||||
var fields = $.map(frappe.get_doc("DocType", "Item").fields, function (d) {
|
||||
if (
|
||||
search_fields_datatypes.includes(d.fieldtype) &&
|
||||
!do_not_include_fields.includes(d.fieldname)
|
||||
) {
|
||||
return [d.label];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
frm.searchable_item_fields = Object.fromEntries(
|
||||
fields.map((df) => [df.option, df.fieldname])
|
||||
);
|
||||
|
||||
frm.fields_dict.pos_search_fields.grid.update_docfield_property(
|
||||
"field",
|
||||
"options",
|
||||
[""].concat(fields.map((df) => df.option))
|
||||
);
|
||||
},
|
||||
fields.unshift("");
|
||||
frm.fields_dict.pos_search_fields.grid.update_docfield_property("field", "options", fields);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("POS Search Fields", {
|
||||
field: function (frm, doctype, name) {
|
||||
const doc = frappe.get_doc(doctype, name);
|
||||
var doc = frappe.get_doc(doctype, name);
|
||||
var df = $.map(frappe.get_doc("DocType", "Item").fields, function (d) {
|
||||
if (doc.field == d.label && search_fields_datatypes.includes(d.fieldtype)) {
|
||||
return d;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})[0];
|
||||
|
||||
doc.fieldname = frm.searchable_item_fields?.[doc.field] || "";
|
||||
frm.refresh_field("pos_search_fields");
|
||||
doc.fieldname = df.fieldname;
|
||||
frm.refresh_field("fields");
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("POS Field", {
|
||||
fieldname: function (frm, doctype, name) {
|
||||
const doc = frappe.get_doc(doctype, name);
|
||||
const invoice_meta = frappe.get_doc("DocType", frm.doc.invoice_type);
|
||||
const df = invoice_meta?.fields.find((d) => d.fieldname === doc.fieldname);
|
||||
if (!df) return;
|
||||
var doc = frappe.get_doc(doctype, name);
|
||||
var df = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function (d) {
|
||||
return doc.fieldname == d.fieldname ? d : null;
|
||||
})[0];
|
||||
|
||||
doc.label = df.label;
|
||||
doc.reqd = df.reqd;
|
||||
doc.options = df.options;
|
||||
doc.fieldtype = df.fieldtype;
|
||||
doc.default_value = df.default;
|
||||
frm.refresh_field("invoice_fields");
|
||||
frm.refresh_field("fields");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,46 +5,8 @@ from collections import Counter
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model import no_value_fields
|
||||
from frappe.model.document import Document
|
||||
|
||||
SEARCH_FIELD_TYPES = (
|
||||
"Data",
|
||||
"Link",
|
||||
"Dynamic Link",
|
||||
"Long Text",
|
||||
"Select",
|
||||
"Small Text",
|
||||
"Text",
|
||||
"Text Editor",
|
||||
)
|
||||
|
||||
# Item fields that are of a searchable fieldtype, but are not meaningful to search a POS item by
|
||||
DO_NOT_INCLUDE_FIELDS = (
|
||||
"naming_series",
|
||||
"item_code",
|
||||
"item_name",
|
||||
"stock_uom",
|
||||
"asset_naming_series",
|
||||
"default_material_request_type",
|
||||
"valuation_method",
|
||||
"warranty_period",
|
||||
"weight_uom",
|
||||
"batch_number_series",
|
||||
"serial_no_series",
|
||||
"purchase_uom",
|
||||
"customs_tariff_number",
|
||||
"sales_uom",
|
||||
"deferred_revenue_account",
|
||||
"deferred_expense_account",
|
||||
"quality_inspection_template",
|
||||
"route",
|
||||
"slideshow",
|
||||
"website_image_alt",
|
||||
"thumbnail",
|
||||
"web_long_description",
|
||||
)
|
||||
|
||||
|
||||
class POSSettings(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -70,10 +32,17 @@ class POSSettings(Document):
|
||||
if old_doc.invoice_type != self.invoice_type:
|
||||
self.validate_invoice_type()
|
||||
|
||||
self.validate_duplicate_invoice_fields()
|
||||
self.validate_invoice_fields()
|
||||
self.validate_duplicate_pos_search_fields()
|
||||
self.validate_pos_search_fields()
|
||||
|
||||
def validate_invoice_fields(self):
|
||||
invoice_fields = [field.fieldname for field in self.invoice_fields]
|
||||
duplicate_invoice_fields = {key for key, value in Counter(invoice_fields).items() if value > 1}
|
||||
|
||||
if len(duplicate_invoice_fields):
|
||||
for field in duplicate_invoice_fields:
|
||||
frappe.throw(
|
||||
title=_("Duplicate POS Fields"), msg=_("'{0}' has been already added.").format(field)
|
||||
)
|
||||
|
||||
def validate_invoice_type(self):
|
||||
pos_opening_entries_count = frappe.db.count(
|
||||
@@ -86,94 +55,3 @@ class POSSettings(Document):
|
||||
),
|
||||
title=_("Invoice Document Type Selection Error"),
|
||||
)
|
||||
|
||||
def validate_duplicate_invoice_fields(self):
|
||||
invoice_fields = [field.fieldname for field in self.invoice_fields]
|
||||
duplicate_invoice_fields = {key for key, value in Counter(invoice_fields).items() if value > 1}
|
||||
|
||||
if len(duplicate_invoice_fields):
|
||||
for field in duplicate_invoice_fields:
|
||||
frappe.throw(
|
||||
title=_("Duplicate POS Fields"), msg=_("'{0}' has been already added.").format(field)
|
||||
)
|
||||
|
||||
def validate_invoice_fields(self):
|
||||
if not self.invoice_type:
|
||||
return
|
||||
|
||||
meta = frappe.get_meta(self.invoice_type)
|
||||
|
||||
for field in self.invoice_fields:
|
||||
df = meta.get_field(field.fieldname)
|
||||
|
||||
if not df or not is_valid_invoice_field(df):
|
||||
frappe.throw(
|
||||
title=_("Invalid POS Field"),
|
||||
msg=_("Row #{0}: '{1}' is not a valid field of {2}.").format(
|
||||
field.idx, frappe.bold(field.fieldname or ""), frappe.bold(_(self.invoice_type))
|
||||
),
|
||||
)
|
||||
|
||||
# read only in the form, so keep them in sync with the invoice
|
||||
field.label = df.label
|
||||
field.fieldtype = df.fieldtype
|
||||
field.options = df.options
|
||||
|
||||
def validate_duplicate_pos_search_fields(self):
|
||||
fieldnames = [field.fieldname for field in self.pos_search_fields]
|
||||
duplicate_fieldnames = {key for key, value in Counter(fieldnames).items() if value > 1}
|
||||
|
||||
for fieldname in duplicate_fieldnames:
|
||||
frappe.throw(
|
||||
title=_("Duplicate POS Search Fields"),
|
||||
msg=_("'{0}' has been already added.").format(fieldname),
|
||||
)
|
||||
|
||||
def validate_pos_search_fields(self):
|
||||
searchable_fields = {df.fieldname: df for df in get_searchable_item_fields()}
|
||||
|
||||
for field in self.pos_search_fields:
|
||||
df = searchable_fields.get(field.fieldname)
|
||||
|
||||
if not df:
|
||||
frappe.throw(
|
||||
title=_("Invalid POS Search Field"),
|
||||
msg=_("Row #{0}: '{1}' cannot be used to search items.").format(
|
||||
field.idx, frappe.bold(field.fieldname or "")
|
||||
),
|
||||
)
|
||||
|
||||
if field.field != get_search_field_option(df):
|
||||
frappe.throw(
|
||||
title=_("Invalid POS Search Field"),
|
||||
msg=_("Row #{0}: '{1}' does not match {2}.").format(
|
||||
field.idx, frappe.bold(field.field or ""), frappe.bold(df.fieldname)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def is_valid_invoice_field(df):
|
||||
return df.fieldtype not in no_value_fields or df.fieldtype == "Button"
|
||||
|
||||
|
||||
def get_searchable_item_fields():
|
||||
return [
|
||||
df
|
||||
for df in frappe.get_meta("Item").fields
|
||||
if df.fieldtype in SEARCH_FIELD_TYPES and df.fieldname not in DO_NOT_INCLUDE_FIELDS
|
||||
]
|
||||
|
||||
|
||||
def get_search_field_option(df):
|
||||
# the fieldname keeps the option unique, two Item fields can share a label
|
||||
return f"{df.label} ({df.fieldname})"
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pos_search_field_options():
|
||||
frappe.has_permission("POS Settings", throw=True)
|
||||
|
||||
return [
|
||||
{"option": get_search_field_option(df), "fieldname": df.fieldname}
|
||||
for df in get_searchable_item_fields()
|
||||
]
|
||||
|
||||
@@ -1,135 +1,8 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.patches.v16_0.append_fieldname_to_pos_search_fields import execute as append_fieldname
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestPOSSettings(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.settings = frappe.get_single("POS Settings")
|
||||
self.settings.invoice_fields = []
|
||||
self.settings.pos_search_fields = []
|
||||
|
||||
def assertInvalid(self, message):
|
||||
with self.assertRaises(frappe.ValidationError) as context:
|
||||
self.settings.save()
|
||||
|
||||
self.assertIn(message, str(context.exception))
|
||||
|
||||
def test_invoice_field_is_validated_against_invoice_type(self):
|
||||
# consolidated_invoice exists on POS Invoice only
|
||||
self.settings.invoice_type = "POS Invoice"
|
||||
self.settings.append("invoice_fields", {"fieldname": "consolidated_invoice"})
|
||||
self.settings.save()
|
||||
|
||||
self.settings.invoice_type = "Sales Invoice"
|
||||
self.assertInvalid("is not a valid field of")
|
||||
|
||||
def test_field_common_to_both_invoice_types_is_allowed(self):
|
||||
for invoice_type in ("POS Invoice", "Sales Invoice"):
|
||||
self.settings.invoice_type = invoice_type
|
||||
self.settings.invoice_fields = []
|
||||
self.settings.append("invoice_fields", {"fieldname": "po_no"})
|
||||
self.settings.save()
|
||||
|
||||
def test_unknown_invoice_field_is_not_allowed(self):
|
||||
self.settings.append("invoice_fields", {"fieldname": "not_a_field"})
|
||||
self.assertInvalid("is not a valid field of")
|
||||
|
||||
def test_layout_invoice_field_is_not_allowed(self):
|
||||
self.settings.append("invoice_fields", {"fieldname": "accounting_dimensions_section"})
|
||||
self.assertInvalid("is not a valid field of")
|
||||
|
||||
def test_invoice_field_properties_are_set_from_the_invoice(self):
|
||||
self.settings.append(
|
||||
"invoice_fields", {"fieldname": "customer", "label": "Tampered", "fieldtype": "Data"}
|
||||
)
|
||||
self.settings.save()
|
||||
|
||||
field = self.settings.invoice_fields[0]
|
||||
self.assertEqual(field.label, "Customer")
|
||||
self.assertEqual(field.fieldtype, "Link")
|
||||
self.assertEqual(field.options, "Customer")
|
||||
|
||||
def test_searchable_item_field_is_allowed(self):
|
||||
self.settings.append(
|
||||
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
|
||||
)
|
||||
self.settings.save()
|
||||
|
||||
self.assertEqual(self.settings.pos_search_fields[0].fieldname, "description")
|
||||
|
||||
def test_excluded_search_field_is_not_allowed(self):
|
||||
self.settings.append(
|
||||
"pos_search_fields", {"field": "Item Name (item_name)", "fieldname": "item_name"}
|
||||
)
|
||||
self.assertInvalid("cannot be used to search items")
|
||||
|
||||
def test_search_field_of_unsearchable_type_is_not_allowed(self):
|
||||
# maintain stock is a Check field
|
||||
self.settings.append(
|
||||
"pos_search_fields", {"field": "Maintain Stock (is_stock_item)", "fieldname": "is_stock_item"}
|
||||
)
|
||||
self.assertInvalid("cannot be used to search items")
|
||||
|
||||
def test_unknown_search_field_is_not_allowed(self):
|
||||
self.settings.append(
|
||||
"pos_search_fields", {"field": "Nope (not_an_item_field)", "fieldname": "not_an_item_field"}
|
||||
)
|
||||
self.assertInvalid("cannot be used to search items")
|
||||
|
||||
def test_search_field_without_a_fieldname_is_not_allowed(self):
|
||||
# the form fills the fieldname in, it cannot be picked on its own
|
||||
self.settings.append("pos_search_fields", {"field": "Description (description)"})
|
||||
self.assertInvalid("cannot be used to search items")
|
||||
|
||||
def test_search_field_option_must_match_its_fieldname(self):
|
||||
self.settings.append("pos_search_fields", {"field": "Brand (brand)", "fieldname": "description"})
|
||||
self.assertInvalid("does not match")
|
||||
|
||||
def test_bare_label_is_not_accepted_as_a_search_field(self):
|
||||
# the stored option carries the fieldname, the patch backfills older rows
|
||||
self.settings.append("pos_search_fields", {"field": "Description", "fieldname": "description"})
|
||||
self.assertInvalid("does not match")
|
||||
|
||||
def test_duplicate_search_fields_are_not_allowed(self):
|
||||
for _ in range(2):
|
||||
self.settings.append(
|
||||
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
|
||||
)
|
||||
|
||||
self.assertInvalid("has been already added")
|
||||
|
||||
def test_patch_appends_the_fieldname_to_a_legacy_search_field(self):
|
||||
self.settings.append(
|
||||
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
|
||||
)
|
||||
self.settings.save()
|
||||
|
||||
row = self.settings.pos_search_fields[0].name
|
||||
frappe.db.set_value("POS Search Fields", row, "field", "Description", update_modified=False)
|
||||
|
||||
append_fieldname()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("POS Search Fields", row, "field"), "Description (description)")
|
||||
|
||||
def test_patch_leaves_an_already_migrated_search_field_alone(self):
|
||||
self.settings.append(
|
||||
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
|
||||
)
|
||||
self.settings.save()
|
||||
|
||||
append_fieldname()
|
||||
|
||||
row = self.settings.pos_search_fields[0].name
|
||||
self.assertEqual(frappe.db.get_value("POS Search Fields", row, "field"), "Description (description)")
|
||||
|
||||
def test_invoice_fields_are_skipped_when_no_invoice_type_is_selected(self):
|
||||
self.settings.invoice_type = None
|
||||
self.settings.append("invoice_fields", {"fieldname": "customer"})
|
||||
self.settings.save()
|
||||
|
||||
self.assertEqual(self.settings.invoice_fields[0].fieldname, "customer")
|
||||
pass
|
||||
|
||||
@@ -187,10 +187,7 @@ class PricingRule(Document):
|
||||
|
||||
tocheck = frappe.scrub(self.get("applicable_for", ""))
|
||||
if tocheck and not self.get(tocheck):
|
||||
throw(
|
||||
_("{0} is required").format(self.meta.get_translated_label(tocheck)),
|
||||
frappe.MandatoryError,
|
||||
)
|
||||
throw(_("{0} is required").format(_(self.meta.get_label(tocheck))), frappe.MandatoryError)
|
||||
|
||||
if self.apply_rule_on_other:
|
||||
o_field = "other_" + frappe.scrub(self.apply_rule_on_other)
|
||||
|
||||
@@ -142,8 +142,6 @@ def start_pcv_processing(docname: str):
|
||||
|
||||
@frappe.whitelist()
|
||||
def pause_pcv_processing(docname: str):
|
||||
frappe.has_permission("Process Period Closing Voucher", ptype="write", doc=docname, throw=True)
|
||||
|
||||
ppcv = qb.DocType("Process Period Closing Voucher")
|
||||
qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.eq(docname)).run()
|
||||
|
||||
@@ -159,8 +157,6 @@ def pause_pcv_processing(docname: str):
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_pcv_processing(docname: str):
|
||||
frappe.has_permission("Process Period Closing Voucher", ptype="cancel", doc=docname, throw=True)
|
||||
|
||||
ppcv = qb.DocType("Process Period Closing Voucher")
|
||||
qb.update(ppcv).set(ppcv.status, "Cancelled").where(ppcv.name.eq(docname)).run()
|
||||
|
||||
@@ -175,8 +171,6 @@ def cancel_pcv_processing(docname: str):
|
||||
|
||||
@frappe.whitelist()
|
||||
def resume_pcv_processing(docname: str):
|
||||
frappe.has_permission("Process Period Closing Voucher", ptype="write", doc=docname, throw=True)
|
||||
|
||||
ppcv = qb.DocType("Process Period Closing Voucher")
|
||||
qb.update(ppcv).set(ppcv.status, "Running").where(ppcv.name.eq(docname)).run()
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.controllers.accounts_controller import merge_taxes
|
||||
from erpnext.controllers.mapper import get_qty_already_mapped
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -53,11 +52,6 @@ def make_purchase_receipt(
|
||||
args = {}
|
||||
args = frappe.parse_json(args)
|
||||
|
||||
mapped_qty_by_item = get_qty_already_mapped(target_doc, "purchase_invoice_item")
|
||||
|
||||
def received_and_mapped_qty(obj):
|
||||
return flt(obj.received_qty) + flt(mapped_qty_by_item.get(obj.name, 0))
|
||||
|
||||
def post_parent_process(source_parent, target_parent):
|
||||
remove_items_with_zero_qty(target_parent)
|
||||
set_missing_values(source_parent, target_parent)
|
||||
@@ -81,13 +75,15 @@ def make_purchase_receipt(
|
||||
or {}
|
||||
)
|
||||
|
||||
pending_qty = flt(obj.qty) - received_and_mapped_qty(obj)
|
||||
|
||||
target.qty = pending_qty - flt(returned_qty_map.get("qty"))
|
||||
target.received_qty = pending_qty
|
||||
target.stock_qty = (pending_qty - flt(returned_qty_map.get("qty"))) * flt(obj.conversion_factor)
|
||||
target.amount = pending_qty * flt(obj.rate)
|
||||
target.base_amount = pending_qty * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
target.qty = flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))
|
||||
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))) * flt(
|
||||
obj.conversion_factor
|
||||
)
|
||||
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
|
||||
target.base_amount = (
|
||||
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
@@ -117,8 +113,7 @@ def make_purchase_receipt(
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: abs(received_and_mapped_qty(doc)) < abs(doc.qty)
|
||||
and select_item(doc),
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
|
||||
@@ -512,7 +512,6 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:doc.currency && doc.currency != erpnext.get_currency(doc.company)",
|
||||
"fieldname": "currency_and_price_list",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Currency and Price List",
|
||||
@@ -1696,7 +1695,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:46.733125",
|
||||
"modified": "2026-08-05 15:40:16.519774",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
@@ -1749,18 +1748,6 @@
|
||||
"read": 1,
|
||||
"role": "Accounts Manager",
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Quality Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -235,9 +235,6 @@ class PurchaseInvoice(BuyingController):
|
||||
"overflow_type": "billing",
|
||||
}
|
||||
]
|
||||
self.closed_source_links = [
|
||||
("Purchase Invoice Item", "pr_detail", "Purchase Receipt Item", "Purchase Receipt")
|
||||
]
|
||||
|
||||
def onload(self):
|
||||
super().onload()
|
||||
@@ -282,7 +279,9 @@ class PurchaseInvoice(BuyingController):
|
||||
self.check_conversion_rate()
|
||||
self.validate_credit_to_acc()
|
||||
self.clear_unallocated_advances("Purchase Invoice Advance", "advances")
|
||||
self.check_purchase_order_on_hold_or_close("purchase_order", exclude_if_field="purchase_receipt")
|
||||
self.check_for_on_hold_or_closed_status(
|
||||
"Purchase Order", "purchase_order", exclude_if_field="purchase_receipt"
|
||||
)
|
||||
self.validate_with_previous_doc()
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||
@@ -298,7 +297,6 @@ class PurchaseInvoice(BuyingController):
|
||||
BillingValidationService(self).validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
|
||||
self.set_status()
|
||||
self.validate_purchase_receipt_if_update_stock()
|
||||
self.validate_exchange_rate_with_purchase_receipt()
|
||||
validate_inter_company_party(
|
||||
self.doctype, self.supplier, self.company, self.inter_company_invoice_reference
|
||||
)
|
||||
@@ -322,47 +320,6 @@ class PurchaseInvoice(BuyingController):
|
||||
if total_billed_qty and total_received_qty:
|
||||
self.per_received = total_received_qty / total_billed_qty * 100
|
||||
|
||||
def validate_exchange_rate_with_purchase_receipt(self):
|
||||
if self.is_internal_transfer() or not erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
return
|
||||
|
||||
stock_items = self.get_stock_items()
|
||||
receipts = {
|
||||
item.purchase_receipt
|
||||
for item in self.items
|
||||
if item.purchase_receipt and item.item_code in stock_items
|
||||
}
|
||||
if not receipts:
|
||||
return
|
||||
|
||||
if frappe.db.get_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"):
|
||||
return
|
||||
|
||||
mismatched = [
|
||||
f"{frappe.bold(row.name)} ({row.conversion_rate})"
|
||||
for row in frappe.get_all(
|
||||
"Purchase Receipt",
|
||||
filters={"name": ("in", list(receipts))},
|
||||
fields=["name", "currency", "conversion_rate"],
|
||||
)
|
||||
if row.currency == self.currency
|
||||
and flt(row.conversion_rate)
|
||||
and flt(row.conversion_rate) != flt(self.conversion_rate)
|
||||
]
|
||||
if not mismatched:
|
||||
return
|
||||
|
||||
frappe.throw(
|
||||
_(
|
||||
"Exchange rate {0} does not match the exchange rate of Purchase Receipt {1}. Use the same exchange rate as the Purchase Receipt or enable {2} in {3} to adjust the landed cost based on this invoice."
|
||||
).format(
|
||||
frappe.bold(self.conversion_rate),
|
||||
", ".join(mismatched),
|
||||
frappe.bold(_("Set Landed Cost Based on Purchase Invoice Rate")),
|
||||
get_link_to_form("Buying Settings", "Buying Settings", _("Buying Settings")),
|
||||
)
|
||||
)
|
||||
|
||||
def validate_invoice_hold(self):
|
||||
if self.is_return:
|
||||
frappe.throw(_("Return Purchase Invoice cannot be held."))
|
||||
@@ -775,7 +732,9 @@ class PurchaseInvoice(BuyingController):
|
||||
super().on_cancel()
|
||||
PurchaseTaxWithholding(self).on_cancel()
|
||||
|
||||
self.check_purchase_order_on_hold_or_close("purchase_order", exclude_if_field="purchase_receipt")
|
||||
self.check_for_on_hold_or_closed_status(
|
||||
"Purchase Order", "purchase_order", exclude_if_field="purchase_receipt"
|
||||
)
|
||||
|
||||
if self.is_return and not self.update_billed_amount_in_purchase_order:
|
||||
# NOTE status updating bypassed for is_return
|
||||
|
||||
@@ -130,9 +130,6 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
|
||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
|
||||
get_purchase_document_details,
|
||||
)
|
||||
from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import (
|
||||
get_custom_dimension_overrides,
|
||||
)
|
||||
|
||||
doc = self.doc
|
||||
tax_service = TaxService(doc)
|
||||
@@ -273,34 +270,25 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
|
||||
|
||||
# Amount added through landed-cost-voucher
|
||||
if landed_cost_entries:
|
||||
for entry in landed_cost_entries.get((item.item_code, item.name), []):
|
||||
if not (entry.amount or entry.base_amount):
|
||||
continue
|
||||
|
||||
lcv_account_currency = get_account_currency(entry.expense_account)
|
||||
credit_in_transaction_currency = (
|
||||
flt(entry.amount)
|
||||
if lcv_account_currency == doc.currency
|
||||
else flt(
|
||||
entry.base_amount / doc.conversion_rate, item.precision("net_amount")
|
||||
if (item.item_code, item.name) in landed_cost_entries:
|
||||
for account, base_amount in landed_cost_entries[
|
||||
(item.item_code, item.name)
|
||||
].items():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": account,
|
||||
"against": item.expense_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(base_amount["base_amount"]),
|
||||
"credit_in_account_currency": flt(base_amount["amount"]),
|
||||
"credit_in_transaction_currency": item.net_amount,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
gl_dict = self.get_gl_dict(
|
||||
{
|
||||
"account": entry.expense_account,
|
||||
"against": item.expense_account,
|
||||
"cost_center": entry.dimensions.cost_center or item.cost_center,
|
||||
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(entry.base_amount),
|
||||
"credit_in_account_currency": flt(entry.amount),
|
||||
"credit_in_transaction_currency": credit_in_transaction_currency,
|
||||
"project": entry.dimensions.project or item.project or doc.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
gl_dict.update(get_custom_dimension_overrides(entry))
|
||||
gl_entries.append(gl_dict)
|
||||
|
||||
# sub-contracting warehouse
|
||||
if flt(item.rm_supp_cost):
|
||||
|
||||
@@ -578,6 +578,10 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
make_purchase_invoice as create_purchase_invoice,
|
||||
)
|
||||
|
||||
original_value = frappe.db.get_single_value(
|
||||
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
@@ -590,15 +594,25 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
pi = create_purchase_invoice(pr.name)
|
||||
pi.conversion_rate = 80
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pi.insert)
|
||||
|
||||
pi.conversion_rate = 70
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
|
||||
# Get exchnage gain and loss account
|
||||
exchange_gain_loss_account = frappe.db.get_value("Company", pi.company, "exchange_gain_loss_account")
|
||||
self.assertFalse(
|
||||
frappe.db.exists("GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name})
|
||||
|
||||
# fetching the latest GL Entry with exchange gain and loss account account
|
||||
amount = frappe.db.get_value(
|
||||
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "debit"
|
||||
)
|
||||
|
||||
discrepancy_caused_by_exchange_rate_diff = abs(
|
||||
pi.items[0].base_net_amount - pr.items[0].base_net_amount
|
||||
)
|
||||
|
||||
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", original_value
|
||||
)
|
||||
|
||||
def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self):
|
||||
@@ -606,8 +620,7 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
make_purchase_invoice as create_purchase_invoice,
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||
|
||||
# Creating Purchase Invoice with USD currency
|
||||
pr = frappe.new_doc("Purchase Receipt")
|
||||
pr.currency = "USD"
|
||||
pr.company = "_Test Company with perpetual inventory"
|
||||
@@ -621,20 +634,34 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
"rate": 100,
|
||||
},
|
||||
)
|
||||
pr.append(
|
||||
"items",
|
||||
{"item_code": "_Test Item", "qty": 1, "rate": 5, "warehouse": "Stores - TCP1"},
|
||||
)
|
||||
pr.insert()
|
||||
pr.submit()
|
||||
|
||||
# Createing purchase invoice against Purchase Receipt
|
||||
pi = create_purchase_invoice(pr.name)
|
||||
pi.conversion_rate = 70
|
||||
pi.credit_to = "_Test Payable USD - TCP1"
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
|
||||
# Get exchnage gain and loss account
|
||||
exchange_gain_loss_account = frappe.db.get_value("Company", pi.company, "exchange_gain_loss_account")
|
||||
self.assertFalse(
|
||||
frappe.db.exists("GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name})
|
||||
|
||||
# fetching the latest GL Entry with exchange gain and loss account account
|
||||
amount = frappe.db.get_value(
|
||||
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "credit"
|
||||
)
|
||||
|
||||
discrepancy_caused_by_exchange_rate_diff = abs(
|
||||
pi.items[1].base_net_amount - pr.items[1].base_net_amount
|
||||
)
|
||||
|
||||
self.assertEqual(flt(discrepancy_caused_by_exchange_rate_diff, 2), amount)
|
||||
|
||||
def test_purchase_invoice_change_naming_series(self):
|
||||
pi = frappe.copy_doc(self.globalTestRecords["Purchase Invoice"][1])
|
||||
pi.insert()
|
||||
@@ -3526,6 +3553,7 @@ def make_purchase_invoice_against_cost_center(**args):
|
||||
|
||||
def setup_provisional_accounting(**args):
|
||||
args = frappe._dict(args)
|
||||
create_item("_Test Non Stock Item", is_stock_item=0)
|
||||
company = args.company or "_Test Company"
|
||||
provisional_account = create_account(
|
||||
account_name=args.account_name or "Provision Account",
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
"icon": "fa fa-money",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:47.506282",
|
||||
"modified": "2024-03-27 13:10:26.945131",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Taxes and Charges Template",
|
||||
@@ -104,29 +104,12 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Accounts User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ def start_payment_ledger_repost(docname: str | None = None):
|
||||
"""
|
||||
if docname:
|
||||
repost_doc = frappe.get_doc("Repost Payment Ledger", docname)
|
||||
repost_doc.check_permission("submit")
|
||||
if repost_doc.docstatus.is_submitted() and repost_doc.repost_status in ["Queued", "Failed"]:
|
||||
try:
|
||||
for entry in repost_doc.repost_vouchers:
|
||||
|
||||
@@ -170,10 +170,6 @@
|
||||
"shipping_address_section",
|
||||
"shipping_address_name",
|
||||
"shipping_address",
|
||||
"shipping_contact_person",
|
||||
"shipping_contact_display",
|
||||
"shipping_contact_mobile",
|
||||
"shipping_contact_email",
|
||||
"shipping_addr_col_break",
|
||||
"dispatch_address_name",
|
||||
"dispatch_address",
|
||||
@@ -593,46 +589,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "shipping_contact_person",
|
||||
"fieldtype": "Link",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Shipping Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "shipping_contact_person.full_name",
|
||||
"fieldname": "shipping_contact_display",
|
||||
"fieldtype": "Small Text",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Shipping Contact",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "shipping_contact_person.mobile_no",
|
||||
"fieldname": "shipping_contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Shipping Contact Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "shipping_contact_person.email_id",
|
||||
"fieldname": "shipping_contact_email",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Shipping Contact Email",
|
||||
"options": "Email",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_address",
|
||||
"fieldtype": "Link",
|
||||
@@ -653,7 +609,6 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:doc.currency && doc.currency != erpnext.get_currency(doc.company)",
|
||||
"depends_on": "customer",
|
||||
"fieldname": "currency_and_price_list",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -2405,7 +2360,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2026-08-14 12:43:19.480555",
|
||||
"modified": "2026-08-11 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -202,10 +202,6 @@ class SalesInvoice(SellingController):
|
||||
set_warehouse: DF.Link | None
|
||||
shipping_address: DF.TextEditor | None
|
||||
shipping_address_name: DF.Link | None
|
||||
shipping_contact_display: DF.SmallText | None
|
||||
shipping_contact_email: DF.Data | None
|
||||
shipping_contact_mobile: DF.SmallText | None
|
||||
shipping_contact_person: DF.Link | None
|
||||
shipping_rule: DF.Link | None
|
||||
status: DF.Literal[
|
||||
"",
|
||||
@@ -278,9 +274,6 @@ class SalesInvoice(SellingController):
|
||||
"overflow_type": "billing",
|
||||
}
|
||||
]
|
||||
self.closed_source_links = [
|
||||
("Sales Invoice Item", "dn_detail", "Delivery Note Item", "Delivery Note")
|
||||
]
|
||||
|
||||
def set_indicator(self):
|
||||
"""Set indicator for portal"""
|
||||
@@ -617,7 +610,6 @@ class SalesInvoice(SellingController):
|
||||
"percent_join_field": "sales_order",
|
||||
"status_field": "delivery_status",
|
||||
"keyword": "Delivered",
|
||||
"exclude_field": "skip_delivery",
|
||||
"second_source_dt": "Delivery Note Item",
|
||||
"second_source_field": "qty",
|
||||
"second_join_field": "so_detail",
|
||||
|
||||
@@ -476,7 +476,7 @@ class SalesInvoiceGLComposer(BaseGLComposer):
|
||||
|
||||
for payment_mode in doc.payments:
|
||||
if skip_change_gl_entries and payment_mode.account == doc.account_for_change_amount:
|
||||
payment_mode.base_amount -= flt(doc.base_change_amount)
|
||||
payment_mode.base_amount -= flt(doc.change_amount)
|
||||
|
||||
if payment_mode.base_amount:
|
||||
# POS, make payment entries
|
||||
|
||||
@@ -7,7 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from frappe.utils import add_days, add_to_date, cint, flt, format_date, getdate, nowdate, today
|
||||
from frappe.utils import add_days, cint, flt, format_date, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
@@ -129,14 +129,14 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
|
||||
w2 = frappe.get_doc(w.doctype, w.name)
|
||||
|
||||
import time
|
||||
|
||||
time.sleep(1)
|
||||
w.save()
|
||||
frappe.db.set_value(
|
||||
w.doctype,
|
||||
w.name,
|
||||
"modified",
|
||||
add_to_date(w.modified, seconds=1),
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
import time
|
||||
|
||||
time.sleep(1)
|
||||
self.assertRaises(frappe.TimestampMismatchError, w2.save)
|
||||
|
||||
def test_sales_invoice_change_naming_series(self):
|
||||
@@ -1583,35 +1583,6 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
|
||||
frappe.db.set_single_value("POS Settings", "post_change_gl_entries", 1)
|
||||
|
||||
def test_pos_change_amount_multi_currency_gl_entry(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.services.gl_composer import SalesInvoiceGLComposer
|
||||
|
||||
frappe.db.set_single_value("POS Settings", "post_change_gl_entries", 0)
|
||||
|
||||
si = create_sales_invoice(do_not_save=True)
|
||||
si.is_pos = 1
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 50
|
||||
si.party_account_currency = "USD"
|
||||
si.account_for_change_amount = "Cash - _TC"
|
||||
si.change_amount = 50
|
||||
si.base_change_amount = 2500
|
||||
si.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 150, "base_amount": 7500},
|
||||
)
|
||||
|
||||
gl_entries = []
|
||||
SalesInvoiceGLComposer(si).make_pos_gl_entries(gl_entries)
|
||||
|
||||
debtors_entry = next(entry for entry in gl_entries if entry["account"] == si.debit_to)
|
||||
cash_entry = next(entry for entry in gl_entries if entry["account"] == "Cash - _TC")
|
||||
|
||||
self.assertEqual(flt(debtors_entry["credit"]), 5000.0)
|
||||
self.assertEqual(flt(cash_entry["debit"]), 5000.0)
|
||||
|
||||
frappe.db.set_single_value("POS Settings", "post_change_gl_entries", 1)
|
||||
|
||||
def test_stock_delivered_but_not_billed_gl_on_invoice(self):
|
||||
company = "_Test SDBNB Company"
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
@@ -3846,12 +3817,25 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
# enable common party accounting
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
|
||||
|
||||
# make the shared department dimension mandatory
|
||||
dim = frappe.get_doc("Accounting Dimension", {"document_type": "Department"})
|
||||
dim.disabled = False
|
||||
dim.dimension_defaults = []
|
||||
dim.append("dimension_defaults", {"company": "_Test Company", "mandatory_for_bs": True})
|
||||
dim.save()
|
||||
# create a dimension and make it mandatory
|
||||
if not frappe.get_all("Accounting Dimension", filters={"document_type": "Department"}):
|
||||
dim = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Accounting Dimension",
|
||||
"document_type": "Department",
|
||||
"dimension_defaults": [{"company": "_Test Company", "mandatory_for_bs": True}],
|
||||
}
|
||||
)
|
||||
dim.save()
|
||||
else:
|
||||
dim = frappe.get_doc(
|
||||
"Accounting Dimension",
|
||||
frappe.get_all("Accounting Dimension", filters={"document_type": "Department"})[0],
|
||||
)
|
||||
dim.disabled = False
|
||||
dim.dimension_defaults = []
|
||||
dim.append("dimension_defaults", {"company": "_Test Company", "mandatory_for_bs": True})
|
||||
dim.save()
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
@@ -5805,6 +5789,12 @@ def create_internal_parties():
|
||||
allowed_to_interact_with="Wind Power LLC",
|
||||
)
|
||||
|
||||
create_internal_customer(
|
||||
customer_name="_Test Internal Customer 2",
|
||||
represents_company="_Test Company with perpetual inventory",
|
||||
allowed_to_interact_with="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
create_internal_customer(
|
||||
customer_name="_Test Internal Customer 3",
|
||||
represents_company="_Test Company",
|
||||
@@ -5825,6 +5815,12 @@ def create_internal_parties():
|
||||
allowed_to_interact_with="_Test Company 1",
|
||||
)
|
||||
|
||||
create_internal_supplier(
|
||||
supplier_name="_Test Internal Supplier 2",
|
||||
represents_company="_Test Company with perpetual inventory",
|
||||
allowed_to_interact_with="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
create_internal_supplier(
|
||||
supplier_name="_Test Internal Customer 3",
|
||||
represents_company="_Test Company",
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"icon": "fa fa-money",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:48.797423",
|
||||
"modified": "2024-03-27 13:10:38.343481",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Taxes and Charges Template",
|
||||
@@ -113,33 +113,12 @@
|
||||
"role": "Sales Master Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,10 @@ def get_data():
|
||||
"non_standard_fieldnames": {
|
||||
"Tax Rule": "sales_tax_template",
|
||||
"Subscription": "sales_tax_template",
|
||||
"Restaurant": "default_tax_template",
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Transactions"), "items": ["Sales Invoice", "Sales Order", "Delivery Note"]},
|
||||
{"label": _("References"), "items": ["POS Profile", "Subscription", "Tax Rule"]},
|
||||
{"label": _("References"), "items": ["POS Profile", "Subscription", "Restaurant", "Tax Rule"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
"icon": "fa fa-truck",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:49.532098",
|
||||
"modified": "2026-07-22 14:53:27.315435",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Shipping Rule",
|
||||
@@ -195,38 +195,6 @@
|
||||
"role": "Sales Master Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.utils.data import (
|
||||
@@ -659,15 +658,23 @@ class TestSubscription(ERPNextTestSuite):
|
||||
sub2 = create_subscription(start_date="2018-01-02")
|
||||
|
||||
processed = []
|
||||
original_process = Subscription.process
|
||||
original_rollback = frappe.db.rollback
|
||||
|
||||
def patched(self, posting_date=None):
|
||||
processed.append(self.name)
|
||||
if self.name == sub1.name:
|
||||
raise frappe.ValidationError("forced failure")
|
||||
|
||||
# Stub transaction recovery so the test can observe the complete iteration in isolation.
|
||||
with patch.object(Subscription, "process", patched), patch.object(frappe.db, "rollback"):
|
||||
Subscription.process = patched
|
||||
# process_all calls frappe.db.rollback() on error which would otherwise wipe
|
||||
# the test transaction; stub it so we can observe the iteration in isolation.
|
||||
frappe.db.rollback = lambda *a, **kw: None
|
||||
try:
|
||||
process_all([sub1.name, sub2.name])
|
||||
finally:
|
||||
Subscription.process = original_process
|
||||
frappe.db.rollback = original_rollback
|
||||
|
||||
self.assertEqual(processed, [sub1.name, sub2.name])
|
||||
|
||||
@@ -1066,6 +1073,12 @@ def create_plan(**kwargs):
|
||||
|
||||
|
||||
def create_parties():
|
||||
if not frappe.db.exists("Supplier", "_Test Supplier"):
|
||||
supplier = frappe.new_doc("Supplier")
|
||||
supplier.supplier_name = "_Test Supplier"
|
||||
supplier.supplier_group = "All Supplier Groups"
|
||||
supplier.insert()
|
||||
|
||||
if not frappe.db.exists("Customer", "_Test Subscription Customer"):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test Subscription Customer"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:50.740962",
|
||||
"modified": "2024-03-27 13:10:51.976600",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Category",
|
||||
@@ -68,68 +68,11 @@
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Item Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,25 @@ class TestTaxRule(ERPNextTestSuite):
|
||||
|
||||
def test_for_parent_supplier_group(self):
|
||||
purchase_template = "_Test Purchase Taxes and Charges Template - _TC"
|
||||
if not frappe.db.exists("Purchase Taxes and Charges Template", purchase_template):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Purchase Taxes and Charges Template",
|
||||
"title": "_Test Purchase Taxes and Charges Template",
|
||||
"company": "_Test Company",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "VAT",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"cost_center": "Main - _TC",
|
||||
"rate": 6,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
make_tax_rule(
|
||||
supplier_group="All Supplier Groups",
|
||||
tax_type="Purchase",
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:50.979676",
|
||||
"modified": "2025-07-30 07:13:51.785735",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Category",
|
||||
@@ -142,26 +142,6 @@
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Item Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:51.176158",
|
||||
"modified": "2025-06-29 05:25:50.243710",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Group",
|
||||
@@ -39,30 +39,6 @@
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Accounts User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -140,7 +140,6 @@ def _get_party_details(
|
||||
if not ignore_permissions:
|
||||
ptype = "select" if frappe.only_has_select_perm(party_type) else "read"
|
||||
frappe.has_permission(party_type, ptype, party, throw=True)
|
||||
validate_party_company(party_type, party.name, company)
|
||||
|
||||
currency = party.get("default_currency") or currency or get_company_currency(company)
|
||||
|
||||
@@ -156,7 +155,7 @@ def _get_party_details(
|
||||
dispatch_address,
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
set_contact_details(party_details, party, party_type, doctype)
|
||||
set_contact_details(party_details, party, party_type)
|
||||
set_other_values(party_details, party, party_type)
|
||||
set_price_list(party_details, party, party_type, price_list, pos_profile)
|
||||
|
||||
@@ -198,17 +197,6 @@ def _get_party_details(
|
||||
return party_details
|
||||
|
||||
|
||||
def validate_party_company(party_type, party, company):
|
||||
if not company or party_type not in ("Customer", "Supplier"):
|
||||
return
|
||||
|
||||
from erpnext.stock.doctype.company_restriction.company_restriction import (
|
||||
validate_masters_for_company,
|
||||
)
|
||||
|
||||
validate_masters_for_company(party_type, [party], company)
|
||||
|
||||
|
||||
def set_address_details(
|
||||
party_details,
|
||||
party,
|
||||
@@ -358,22 +346,10 @@ def complete_contact_details(party_details):
|
||||
party_details.update(contact_details)
|
||||
|
||||
|
||||
def set_contact_details(party_details, party, party_type, doctype=None):
|
||||
def set_contact_details(party_details, party, party_type):
|
||||
party_details.contact_person = get_default_contact(party_type, party.name)
|
||||
complete_contact_details(party_details)
|
||||
|
||||
# the shipping contact is picked by the user, so it has no default to fall back on;
|
||||
# blank it instead of carrying the previous party's contact over
|
||||
if doctype and frappe.get_meta(doctype).has_field("shipping_contact_person"):
|
||||
party_details.update(
|
||||
{
|
||||
"shipping_contact_person": None,
|
||||
"shipping_contact_display": None,
|
||||
"shipping_contact_mobile": None,
|
||||
"shipping_contact_email": None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def set_other_values(party_details, party, party_type):
|
||||
# copy
|
||||
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user